diff --git a/.configure b/.configure index 7053595708b1..d6e9f159bcec 100644 --- a/.configure +++ b/.configure @@ -1,7 +1,7 @@ { "project_name": "WordPress-Android", "branch": "trunk", - "pinned_hash": "91e71c2268b4df54591ff9cedbfd03ac93ba865d", + "pinned_hash": "715a5a119a334ec1ef16b5a6bd77c52094144813", "files_to_copy": [ { "file": "android/WPAndroid/gradle.properties", diff --git a/.configure-files/gradle.properties.enc b/.configure-files/gradle.properties.enc index fc70c7ed4141..c2c83c9d1071 100644 Binary files a/.configure-files/gradle.properties.enc and b/.configure-files/gradle.properties.enc differ diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index f7ef4c88e694..1d298b006c2f 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -11,6 +11,7 @@ * [*] Block Editor: In the deeply nested block warning, only display the ungroup option for blocks that support it [https://github.com/WordPress/gutenberg/pull/56445] * [**] Enable Optimize Image setting (via Me → App Settings) by default, and change default compression and resolution values. [https://github.com/wordpress-mobile/WordPress-Android/pull/19581] * [*] Fixed an issue that prevented theme installation on atomic sites [https://github.com/wordpress-mobile/WordPress-Android/pull/19668] +* [*] Fixed an issue with the pagination of the Blogging Prompts response list [https://github.com/wordpress-mobile/WordPress-Android/pull/19730] 23.7 ----- diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 0b810da286a3..a982ada743d9 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -142,6 +142,8 @@ android { buildConfigField "boolean", "ENABLE_DOMAIN_MANAGEMENT_FEATURE", "false" buildConfigField "boolean", "PLANS_IN_SITE_CREATION", "false" buildConfigField "boolean", "READER_IMPROVEMENTS", "false" + buildConfigField "boolean", "BLOGANUARY_DASHBOARD_NUDGE", "false" + buildConfigField "boolean", "IN_APP_REVIEWS", "false" // Override these constants in jetpack product flavor to enable/ disable features buildConfigField "boolean", "ENABLE_SITE_CREATION", "true" @@ -420,14 +422,8 @@ dependencies { implementation "androidx.lifecycle:lifecycle-process:$androidxLifecycleVersion" implementation "com.android.volley:volley:$androidVolleyVersion" implementation "com.google.android.gms:play-services-auth:$googlePlayServicesAuthVersion" + implementation "com.google.android.gms:play-services-code-scanner:$googlePlayServicesCodeScannerVersion" implementation "com.google.mlkit:barcode-scanning-common:$googleMLKitBarcodeScanningVersion" - implementation "com.google.mlkit:text-recognition:$mlkitTextRecognitionVersion" - implementation "com.google.mlkit:barcode-scanning:$mlkitBarcodeScanningVersion" - - // CameraX - implementation "androidx.camera:camera-camera2:$androidxCameraVersion" - implementation "androidx.camera:camera-lifecycle:$androidxCameraVersion" - implementation "androidx.camera:camera-view:$androidxCameraVersion" implementation "com.android.installreferrer:installreferrer:$androidInstallReferrerVersion" implementation "com.github.chrisbanes:PhotoView:$chrisbanesPhotoviewVersion" diff --git a/WordPress/jetpack_metadata/PlayStoreStrings.po b/WordPress/jetpack_metadata/PlayStoreStrings.po index 8af97954bc06..305fec1d0d17 100644 --- a/WordPress/jetpack_metadata/PlayStoreStrings.po +++ b/WordPress/jetpack_metadata/PlayStoreStrings.po @@ -10,6 +10,14 @@ msgstr "" "X-Generator: VsCode\n" "Project-Id-Version: Jetpack - Apps - Android - Release Notes\n" +msgctxt "release_note_238" +msgid "" +"23.8:\n" +"- Block editor only shows the “ungroup” option for nested blocks that support it.\n" +"- Optimize Images setting uses optimal size and quality by default.\n" +"- Themes install properly for sites on Business and Commerce plans.\n" +msgstr "" + msgctxt "release_note_237" msgid "" "23.7:\n" @@ -18,15 +26,6 @@ msgid "" "- We fixed block editor issues caused by pasting deeply nested content, or using text colors in older site themes.\n" msgstr "" -msgctxt "release_note_236" -msgid "" -"23.6:\n" -"- New domain/plan options and checkout screens in site creation process\n" -"- My Site navigation has links to common sections\n" -"- Inactive social icons and Synced Pattern titles are visible in dark mode for block-based themes\n" -"- Content can be converted into blocks in the Classic editor\n" -msgstr "" - #. translators: Release notes for this version to be displayed in the Play Store. Limit to 500 characters including spaces and commas! #. translators: Title to be displayed in the Play Store. Limit to 30 characters including spaces and commas! msgctxt "play_store_app_title" diff --git a/WordPress/jetpack_metadata/release_notes.txt b/WordPress/jetpack_metadata/release_notes.txt index a3f83889f49e..c1beb73308d2 100644 --- a/WordPress/jetpack_metadata/release_notes.txt +++ b/WordPress/jetpack_metadata/release_notes.txt @@ -1,6 +1,3 @@ -* [*] [Jetpack-only] Block Editor: Ensure text is always visible within Contact Info block [https://github.com/Automattic/jetpack/pull/33873] -* [*] Block Editor: Ensure uploaded audio is always visible within Audio block [https://github.com/WordPress/gutenberg/pull/55627] -* [*] Block Editor: In the deeply nested block warning, only display the ungroup option for blocks that support it [https://github.com/WordPress/gutenberg/pull/56445] -* [**] Enable Optimize Image setting (via Me → App Settings) by default, and change default compression and resolution values. [https://github.com/wordpress-mobile/WordPress-Android/pull/19581] -* [*] Fixed an issue that prevented theme installation on atomic sites [https://github.com/wordpress-mobile/WordPress-Android/pull/19668] - +- Block editor only shows the “ungroup” option for nested blocks that support it. +- Optimize Images setting uses optimal size and quality by default. +- Themes install properly for sites on Business and Commerce plans. diff --git a/WordPress/metadata/PlayStoreStrings.po b/WordPress/metadata/PlayStoreStrings.po index b5cce4a9355b..fe25e2b80de6 100644 --- a/WordPress/metadata/PlayStoreStrings.po +++ b/WordPress/metadata/PlayStoreStrings.po @@ -10,6 +10,14 @@ msgstr "" "X-Generator: VsCode\n" "Project-Id-Version: Release Notes & Play Store Descriptions\n" +msgctxt "release_note_238" +msgid "" +"23.8:\n" +"- Block editor only shows the “ungroup” option for nested blocks that support it.\n" +"- Optimize Images setting uses optimal size and quality by default.\n" +"- Themes install properly for sites on Business and Commerce plans.\n" +msgstr "" + msgctxt "release_note_237" msgid "" "23.7:\n" @@ -17,13 +25,6 @@ msgid "" "- We fixed block editor issues caused by pasting deeply nested content, or using text colors in older site themes.\n" msgstr "" -msgctxt "release_note_236" -msgid "" -"23.6:\n" -"- Inactive social icons and Synced Pattern titles are no longer invisible in block-based themes using dark mode. Good try, fellas, but you’re not as ninja as you think.\n" -"- In the Classic editor, you can now convert your content into blocks with the click of a button.\n" -msgstr "" - #. translators: Release notes for this version to be displayed in the Play Store. Limit to 500 characters including spaces and commas! #. translators: Shorter Release notes for this version to be displayed in the Play Store. Limit to 500 characters including spaces and commas! msgctxt "sample_post_content" diff --git a/WordPress/metadata/release_notes.txt b/WordPress/metadata/release_notes.txt index f21458272bb3..c1beb73308d2 100644 --- a/WordPress/metadata/release_notes.txt +++ b/WordPress/metadata/release_notes.txt @@ -1,4 +1,3 @@ -* [*] Block Editor: Ensure uploaded audio is always visible within Audio block [https://github.com/WordPress/gutenberg/pull/55627] -* [*] Block Editor: In the deeply nested block warning, only display the ungroup option for blocks that support it [https://github.com/WordPress/gutenberg/pull/56445] -* [**] Enable Optimize Image setting (via Me → App Settings) by default, and change default compression and resolution values. [https://github.com/wordpress-mobile/WordPress-Android/pull/19581] -* [*] Fixed an issue that prevented theme installation on atomic sites [https://github.com/wordpress-mobile/WordPress-Android/pull/19668] +- Block editor only shows the “ungroup” option for nested blocks that support it. +- Optimize Images setting uses optimal size and quality by default. +- Themes install properly for sites on Business and Commerce plans. diff --git a/WordPress/src/androidTest/java/org/wordpress/android/editor/savedinstance/ParcelableObjectTest.kt b/WordPress/src/androidTest/java/org/wordpress/android/editor/savedinstance/ParcelableObjectTest.kt new file mode 100644 index 000000000000..4f7b31d85b6d --- /dev/null +++ b/WordPress/src/androidTest/java/org/wordpress/android/editor/savedinstance/ParcelableObjectTest.kt @@ -0,0 +1,42 @@ +package org.wordpress.android.editor.savedinstance + +import android.os.Parcelable +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +@HiltAndroidTest +class ParcelableObjectTest { + private lateinit var parcelable: Parcelable + + @Before + fun setUp() { + parcelable = TestParcelable("testData") + } + + @Test + fun testConstructorWithParcelable() { + val parcelableObject = ParcelableObject(parcelable) + val parcelData = parcelableObject.toBytes() + val objectFromParcel = ParcelableObject(parcelData) + assertArrayEquals(objectFromParcel.toBytes(), parcelData) + } + + @Test + fun testConstructorWithByteArray() { + val parcelableObject = ParcelableObject(parcelable) + val data = parcelableObject.toBytes() + val parcelableResult = ParcelableObject(data) + assertArrayEquals(parcelableObject.toBytes(), parcelableResult.toBytes()) + } + + @Test + fun getParcelReturnsTheSameParcelObject() { + val parcelableObject = ParcelableObject(parcelable) + val initialParcel = parcelableObject.getParcel() + val nextParcel = parcelableObject.getParcel() + assertEquals(initialParcel, nextParcel) + } +} diff --git a/WordPress/src/androidTest/java/org/wordpress/android/editor/savedinstance/SavedInstanceDatabaseTest.kt b/WordPress/src/androidTest/java/org/wordpress/android/editor/savedinstance/SavedInstanceDatabaseTest.kt new file mode 100644 index 000000000000..fdc66073f0fd --- /dev/null +++ b/WordPress/src/androidTest/java/org/wordpress/android/editor/savedinstance/SavedInstanceDatabaseTest.kt @@ -0,0 +1,67 @@ +package org.wordpress.android.editor.savedinstance + +import android.content.Context +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.parcelize.parcelableCreator +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class SavedInstanceDatabaseTest { + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var context: Context + + private lateinit var db: SavedInstanceDatabase + + @Before + fun setUp() { + hiltRule.inject() + db = SavedInstanceDatabase.getDatabase(context)!! + db.reset(db.writableDatabase) + } + + @Test + fun testStoreAndRetrieve() { + val parcelId = "testParcelId" + val parcelData = TestParcelable("testData") + db.addParcel(parcelId, parcelData) + val result = db.getParcel(parcelId, parcelableCreator()) + assertEquals(parcelData, result) + } + + @Test + fun testHasParcel() { + val parcelId = "testParcelId" + val parcelData = TestParcelable("testData") + db.addParcel(parcelId, parcelData) + val result = db.hasParcel(parcelId) + assertTrue(result) + } + + @Test + fun testHasNoParcel() { + val parcelId1 = "testParcelId1" + val parcelId2 = "testParcelId2" + val parcelData = TestParcelable("testData") + db.addParcel(parcelId1, parcelData) + val result = db.hasParcel(parcelId2) + assertFalse(result) + } + + @Test + fun testNullParcel() { + val parcelId = "testParcelId" + db.addParcel(parcelId, null) + val result = db.hasParcel(parcelId) + assertFalse(result) + } +} diff --git a/WordPress/src/androidTest/java/org/wordpress/android/editor/savedinstance/TestParcelable.kt b/WordPress/src/androidTest/java/org/wordpress/android/editor/savedinstance/TestParcelable.kt new file mode 100644 index 000000000000..e097b286be0b --- /dev/null +++ b/WordPress/src/androidTest/java/org/wordpress/android/editor/savedinstance/TestParcelable.kt @@ -0,0 +1,7 @@ +package org.wordpress.android.editor.savedinstance + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TestParcelable(val data: String) : Parcelable diff --git a/WordPress/src/main/java/org/wordpress/android/modules/CodeScannerModule.kt b/WordPress/src/main/java/org/wordpress/android/modules/CodeScannerModule.kt deleted file mode 100644 index 4584657f8a11..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/modules/CodeScannerModule.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.wordpress.android.modules - -import com.google.mlkit.vision.barcode.BarcodeScanner -import com.google.mlkit.vision.barcode.BarcodeScanning -import dagger.Module -import dagger.Provides -import dagger.Reusable -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import org.wordpress.android.ui.barcodescanner.CodeScanner -import org.wordpress.android.ui.barcodescanner.GoogleBarcodeFormatMapper -import org.wordpress.android.ui.barcodescanner.GoogleCodeScannerErrorMapper -import org.wordpress.android.ui.barcodescanner.GoogleMLKitCodeScanner -import org.wordpress.android.ui.barcodescanner.MediaImageProvider - -@InstallIn(SingletonComponent::class) -@Module -class CodeScannerModule { - @Provides - @Reusable - fun provideGoogleCodeScanner( - barcodeScanner: BarcodeScanner, - googleCodeScannerErrorMapper: GoogleCodeScannerErrorMapper, - barcodeFormatMapper: GoogleBarcodeFormatMapper, - inputImageProvider: MediaImageProvider, - ): CodeScanner { - return GoogleMLKitCodeScanner( - barcodeScanner, - googleCodeScannerErrorMapper, - barcodeFormatMapper, - inputImageProvider, - ) - } - - @Provides - @Reusable - fun providesGoogleBarcodeScanner() = BarcodeScanning.getClient() -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt deleted file mode 100644 index e5df791ee239..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanner.kt +++ /dev/null @@ -1,99 +0,0 @@ -package org.wordpress.android.ui.barcodescanner - -import android.content.res.Configuration -import android.util.Size -import androidx.camera.core.CameraSelector -import androidx.camera.core.ImageAnalysis -import androidx.camera.core.ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST -import androidx.camera.core.ImageProxy -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.camera.view.PreviewView -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import org.wordpress.android.ui.compose.theme.AppTheme -import androidx.camera.core.Preview as CameraPreview - -@Composable -fun BarcodeScanner( - codeScanner: CodeScanner, - onScannedResult: (Flow) -> Unit -) { - val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - val cameraProviderFuture = remember { - ProcessCameraProvider.getInstance(context) - } - Column( - modifier = Modifier.fillMaxSize() - ) { - AndroidView( - factory = { context -> - val previewView = PreviewView(context) - val preview = CameraPreview.Builder().build() - preview.setSurfaceProvider(previewView.surfaceProvider) - val selector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() - val imageAnalysis = ImageAnalysis.Builder().setTargetResolution( - Size( - previewView.width, - previewView.height - ) - ) - .setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) - .build() - imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy -> - onScannedResult(codeScanner.startScan(imageProxy)) - } - try { - cameraProviderFuture.get().bindToLifecycle(lifecycleOwner, selector, preview, imageAnalysis) - } catch (e: IllegalStateException) { - onScannedResult( - flowOf( - CodeScannerStatus.Failure( - e.message - ?: "Illegal state exception while binding camera provider to lifecycle", - CodeScanningErrorType.Other(e) - ) - ) - ) - } catch (e: IllegalArgumentException) { - onScannedResult( - flowOf( - CodeScannerStatus.Failure( - e.message - ?: "Illegal argument exception while binding camera provider to lifecycle", - CodeScanningErrorType.Other(e) - ) - ) - ) - } - previewView - }, - modifier = Modifier.fillMaxSize() - ) - } -} - -class DummyCodeScanner : CodeScanner { - override fun startScan(imageProxy: ImageProxy): Flow { - return flowOf(CodeScannerStatus.Success("", GoogleBarcodeFormatMapper.BarcodeFormat.FormatUPCA)) - } -} - -@Preview(name = "Light mode") -@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun BarcodeScannerScreenPreview() { - AppTheme { - BarcodeScanner(codeScanner = DummyCodeScanner(), onScannedResult = {}) - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt deleted file mode 100644 index 01a270db9ece..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScannerScreen.kt +++ /dev/null @@ -1,146 +0,0 @@ -package org.wordpress.android.ui.barcodescanner - -import android.content.res.Configuration -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.padding -import androidx.compose.material.AlertDialog -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import org.wordpress.android.R -import kotlinx.coroutines.flow.Flow -import org.wordpress.android.ui.compose.theme.AppTheme - -@Composable -fun BarcodeScannerScreen( - codeScanner: CodeScanner, - permissionState: BarcodeScanningViewModel.PermissionState, - onResult: (Boolean) -> Unit, - onScannedResult: (Flow) -> Unit, -) { - val cameraPermissionLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - onResult = { granted -> - onResult(granted) - }, - ) - LaunchedEffect(key1 = Unit) { - cameraPermissionLauncher.launch(BarcodeScanningFragment.KEY_CAMERA_PERMISSION) - } - when (permissionState) { - BarcodeScanningViewModel.PermissionState.Granted -> { - BarcodeScanner( - codeScanner = codeScanner, - onScannedResult = onScannedResult - ) - } - is BarcodeScanningViewModel.PermissionState.ShouldShowRationale -> { - AlertDialog( - title = stringResource(id = permissionState.title), - message = stringResource(id = permissionState.message), - ctaLabel = stringResource(id = permissionState.ctaLabel), - dismissCtaLabel = stringResource(id = permissionState.dismissCtaLabel), - ctaAction = { permissionState.ctaAction.invoke(cameraPermissionLauncher) }, - dismissCtaAction = { permissionState.dismissCtaAction.invoke() } - ) - } - is BarcodeScanningViewModel.PermissionState.PermanentlyDenied -> { - AlertDialog( - title = stringResource(id = permissionState.title), - message = stringResource(id = permissionState.message), - ctaLabel = stringResource(id = permissionState.ctaLabel), - dismissCtaLabel = stringResource(id = permissionState.dismissCtaLabel), - ctaAction = { permissionState.ctaAction.invoke(cameraPermissionLauncher) }, - dismissCtaAction = { permissionState.dismissCtaAction.invoke() } - ) - } - BarcodeScanningViewModel.PermissionState.Unknown -> { - // no-op - } - } -} - -@Composable -private fun AlertDialog( - title: String, - message: String, - ctaLabel: String, - dismissCtaLabel: String, - ctaAction: () -> Unit, - dismissCtaAction: () -> Unit, -) { - AlertDialog( - onDismissRequest = { dismissCtaAction() }, - title = { - Text(title) - }, - text = { - Text(message) - }, - confirmButton = { - TextButton( - onClick = { - ctaAction() - } - ) { - Text( - ctaLabel, - color = MaterialTheme.colors.secondary, - modifier = Modifier.padding(8.dp) - ) - } - }, - dismissButton = { - TextButton( - onClick = { - dismissCtaAction() - } - ) { - Text( - dismissCtaLabel, - color = MaterialTheme.colors.secondary, - modifier = Modifier.padding(8.dp) - ) - } - }, - ) -} - -@Preview(name = "Light mode") -@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun DeniedOnceAlertDialog() { - AppTheme { - AlertDialog( - title = stringResource(id = R.string.barcode_scanning_alert_dialog_title), - message = stringResource(id = R.string.barcode_scanning_alert_dialog_rationale_message), - ctaLabel = stringResource(id = R.string.barcode_scanning_alert_dialog_rationale_cta_label), - dismissCtaLabel = stringResource(id = R.string.barcode_scanning_alert_dialog_dismiss_label), - ctaAction = {}, - dismissCtaAction = {}, - ) - } -} - -@Preview(name = "Light mode") -@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun DeniedPermanentlyAlertDialog() { - AppTheme { - AlertDialog( - title = stringResource(id = R.string.barcode_scanning_alert_dialog_title), - message = stringResource(id = R.string.barcode_scanning_alert_dialog_permanently_denied_message), - ctaLabel = stringResource(id = R.string.barcode_scanning_alert_dialog_permanently_denied_cta_label), - dismissCtaLabel = stringResource(id = R.string.barcode_scanning_alert_dialog_dismiss_label), - ctaAction = {}, - dismissCtaAction = {}, - ) - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt deleted file mode 100644 index a05fd7c67769..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningFragment.kt +++ /dev/null @@ -1,102 +0,0 @@ -package org.wordpress.android.ui.barcodescanner - -import android.Manifest -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.addCallback -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import androidx.fragment.app.setFragmentResult -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import org.wordpress.android.ui.compose.theme.AppTheme -import org.wordpress.android.util.WPPermissionUtils -import javax.inject.Inject - -@AndroidEntryPoint -class BarcodeScanningFragment : Fragment() { - private val viewModel: BarcodeScanningViewModel by viewModels() - - @Inject - lateinit var codeScanner: GoogleMLKitCodeScanner - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = - ComposeView(requireContext()) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - view as ComposeView - view.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - observeCameraPermissionState(view) - observeViewModelEvents() - initBackPressHandler() - } - - private fun observeCameraPermissionState(view: ComposeView) { - viewModel.permissionState.observe(viewLifecycleOwner) { permissionState -> - view.setContent { - AppTheme { - BarcodeScannerScreen( - codeScanner = codeScanner, - permissionState = permissionState, - onResult = { granted -> - viewModel.updatePermissionState( - granted, - shouldShowRequestPermissionRationale(KEY_CAMERA_PERMISSION) - ) - }, - onScannedResult = { codeScannerStatus -> - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { - codeScannerStatus.collect { status -> - setResultAndPopStack(status) - } - } - } - }, - ) - } - } - } - } - private fun observeViewModelEvents() { - viewModel.event.observe(viewLifecycleOwner) { event -> - when (event) { - is BarcodeScanningViewModel.ScanningEvents.LaunchCameraPermission -> { - event.cameraLauncher.launch(KEY_CAMERA_PERMISSION) - } - - is BarcodeScanningViewModel.ScanningEvents.OpenAppSettings -> { - WPPermissionUtils.showAppSettings(requireContext()) - } - - is BarcodeScanningViewModel.ScanningEvents.Exit -> { - setResultAndPopStack(CodeScannerStatus.Exit) - } - } - } - } - - private fun initBackPressHandler() { - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { - setResultAndPopStack(CodeScannerStatus.NavigateUp) } - } - - private fun setResultAndPopStack(status: CodeScannerStatus) { - setFragmentResult(KEY_BARCODE_SCANNING_REQUEST, bundleOf(KEY_BARCODE_SCANNING_SCAN_STATUS to status)) - requireActivity().supportFragmentManager.popBackStackImmediate() - } - - companion object { - const val KEY_BARCODE_SCANNING_SCAN_STATUS = "barcode_scanning_scan_status" - const val KEY_BARCODE_SCANNING_REQUEST = "key_barcode_scanning_request" - const val KEY_CAMERA_PERMISSION = Manifest.permission.CAMERA - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningTracker.kt deleted file mode 100644 index c6455ddeed40..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningTracker.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.wordpress.android.ui.barcodescanner - -import org.wordpress.android.analytics.AnalyticsTracker -import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper -import javax.inject.Inject - -class BarcodeScanningTracker @Inject constructor( - private val analyticsTrackerWrapper: AnalyticsTrackerWrapper -) { - fun trackScanFailure(source: ScanningSource, type: CodeScanningErrorType) { - analyticsTrackerWrapper.track( - AnalyticsTracker.Stat.BARCODE_SCANNING_FAILURE, - mapOf( - KEY_SCANNING_SOURCE to source.source, - KEY_SCANNING_FAILURE_REASON to type.toString(), - ) - ) - } - - fun trackSuccess(source: ScanningSource) { - analyticsTrackerWrapper.track( - AnalyticsTracker.Stat.BARCODE_SCANNING_SUCCESS, - mapOf( - KEY_SCANNING_SOURCE to source.source - ) - ) - } - - companion object { - const val KEY_SCANNING_SOURCE = "source" - const val KEY_SCANNING_FAILURE_REASON = "scanning_failure_reason" - } -} - -enum class ScanningSource(val source: String) { - QRCODE_LOGIN("qrcode_login") -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningViewModel.kt deleted file mode 100644 index 532bc34f3ccd..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/BarcodeScanningViewModel.kt +++ /dev/null @@ -1,107 +0,0 @@ -package org.wordpress.android.ui.barcodescanner - -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.annotation.StringRes -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineDispatcher -import org.wordpress.android.R -import org.wordpress.android.modules.BG_THREAD -import org.wordpress.android.viewmodel.ScopedViewModel -import javax.inject.Inject -import javax.inject.Named - -@HiltViewModel -class BarcodeScanningViewModel @Inject constructor( - @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher -) : ScopedViewModel(bgDispatcher) { - private val _permissionState = MutableLiveData() - val permissionState: LiveData = _permissionState - - private val _event: MutableLiveData = MutableLiveData() - val event: LiveData = _event - - init { - _permissionState.value = PermissionState.Unknown - } - - fun updatePermissionState( - isPermissionGranted: Boolean, - shouldShowRequestPermissionRationale: Boolean, - ) { - when { - isPermissionGranted -> { - // display scanning screen - _permissionState.value = PermissionState.Granted - } - - // It will launch events that some place can response to - shouldShowRequestPermissionRationale -> { - // Denied once, ask to grant camera permission - _permissionState.value = PermissionState.ShouldShowRationale( - title = R.string.barcode_scanning_alert_dialog_title, - message = R.string.barcode_scanning_alert_dialog_rationale_message, - ctaLabel = R.string.barcode_scanning_alert_dialog_rationale_cta_label, - dismissCtaLabel = R.string.barcode_scanning_alert_dialog_dismiss_label, - ctaAction = { _event.value = ScanningEvents.LaunchCameraPermission(it) }, - dismissCtaAction = { - _event.value = (ScanningEvents.Exit) - } - ) - } - - else -> { - // Permanently denied, ask to enable permission from the app settings - _permissionState.value = PermissionState.PermanentlyDenied( - title = R.string.barcode_scanning_alert_dialog_title, - message = R.string.barcode_scanning_alert_dialog_permanently_denied_message, - ctaLabel = R.string.barcode_scanning_alert_dialog_permanently_denied_cta_label, - dismissCtaLabel = R.string.barcode_scanning_alert_dialog_dismiss_label, - ctaAction = { - _event.value = ScanningEvents.OpenAppSettings(it) - }, - dismissCtaAction = { - _event.value = (ScanningEvents.Exit) - } - ) - } - } - } - - sealed class ScanningEvents { - data class LaunchCameraPermission( - val cameraLauncher: ManagedActivityResultLauncher - ) : ScanningEvents() - - data class OpenAppSettings( - val cameraLauncher: ManagedActivityResultLauncher - ) : ScanningEvents() - - object Exit : ScanningEvents() - } - - sealed class PermissionState { - object Granted : PermissionState() - - data class ShouldShowRationale( - @StringRes val title: Int, - @StringRes val message: Int, - @StringRes val ctaLabel: Int, - @StringRes val dismissCtaLabel: Int, - val ctaAction: (ManagedActivityResultLauncher) -> Unit, - val dismissCtaAction: () -> Unit, - ) : PermissionState() - - data class PermanentlyDenied( - @StringRes val title: Int, - @StringRes val message: Int, - @StringRes val ctaLabel: Int, - @StringRes val dismissCtaLabel: Int, - val ctaAction: (ManagedActivityResultLauncher) -> Unit, - val dismissCtaAction: () -> Unit, - ) : PermissionState() - - object Unknown : PermissionState() - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt deleted file mode 100644 index 085841613128..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/CodeScanner.kt +++ /dev/null @@ -1,93 +0,0 @@ -package org.wordpress.android.ui.barcodescanner - -import android.os.Parcelable -import androidx.camera.core.ImageProxy -import kotlinx.coroutines.flow.Flow -import kotlinx.parcelize.Parcelize - -interface CodeScanner { - fun startScan(imageProxy: ImageProxy): Flow -} - -sealed class CodeScannerStatus : Parcelable { - @Parcelize - data class Success(val code: String, val format: GoogleBarcodeFormatMapper.BarcodeFormat) : CodeScannerStatus() - @Parcelize - data class Failure( - val error: String?, - val type: CodeScanningErrorType - ) : CodeScannerStatus() - @Parcelize - data object NavigateUp : CodeScannerStatus() - @Parcelize - data object Exit : CodeScannerStatus() -} - -sealed class CodeScanningErrorType : Parcelable { - @Parcelize - object Aborted : CodeScanningErrorType() - @Parcelize - object AlreadyExists : CodeScanningErrorType() - @Parcelize - object Cancelled : CodeScanningErrorType() - @Parcelize - object CodeScannerAppNameUnavailable : CodeScanningErrorType() - @Parcelize - object CodeScannerCameraPermissionNotGranted : CodeScanningErrorType() - @Parcelize - object CodeScannerCancelled : CodeScanningErrorType() - @Parcelize - object CodeScannerGooglePlayServicesVersionTooOld : CodeScanningErrorType() - @Parcelize - object CodeScannerPipelineInferenceError : CodeScanningErrorType() - @Parcelize - object CodeScannerPipelineInitializationError : CodeScanningErrorType() - @Parcelize - object CodeScannerTaskInProgress : CodeScanningErrorType() - @Parcelize - object CodeScannerUnavailable : CodeScanningErrorType() - @Parcelize - object DataLoss : CodeScanningErrorType() - @Parcelize - object DeadlineExceeded : CodeScanningErrorType() - @Parcelize - object FailedPrecondition : CodeScanningErrorType() - @Parcelize - object Internal : CodeScanningErrorType() - @Parcelize - object InvalidArgument : CodeScanningErrorType() - @Parcelize - object ModelHashMismatch : CodeScanningErrorType() - @Parcelize - object ModelIncompatibleWithTFLite : CodeScanningErrorType() - @Parcelize - object NetworkIssue : CodeScanningErrorType() - @Parcelize - object NotEnoughSpace : CodeScanningErrorType() - @Parcelize - object NotFound : CodeScanningErrorType() - @Parcelize - object OutOfRange : CodeScanningErrorType() - @Parcelize - object PermissionDenied : CodeScanningErrorType() - @Parcelize - object ResourceExhausted : CodeScanningErrorType() - @Parcelize - object UnAuthenticated : CodeScanningErrorType() - @Parcelize - object UnAvailable : CodeScanningErrorType() - @Parcelize - object UnImplemented : CodeScanningErrorType() - @Parcelize - object Unknown : CodeScanningErrorType() - @Parcelize - data class Other(val throwable: Throwable?) : CodeScanningErrorType() - - override fun toString(): String = when (this) { - is Other -> this.throwable?.message ?: "Other" - else -> this.javaClass.run { - name.removePrefix("${`package`?.name ?: ""}.") - } - } -} - diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleBarcodeFormatMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleBarcodeFormatMapper.kt deleted file mode 100644 index ef7f9973f399..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleBarcodeFormatMapper.kt +++ /dev/null @@ -1,60 +0,0 @@ -package org.wordpress.android.ui.barcodescanner - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import javax.inject.Inject -import com.google.mlkit.vision.barcode.common.Barcode as GoogleBarcode - -class GoogleBarcodeFormatMapper @Inject constructor() { - @Suppress("ComplexMethod") - fun mapBarcodeFormat(format: Int): BarcodeFormat { - return when (format) { - GoogleBarcode.FORMAT_AZTEC -> BarcodeFormat.FormatAztec - GoogleBarcode.FORMAT_CODABAR -> BarcodeFormat.FormatCodaBar - GoogleBarcode.FORMAT_CODE_128 -> BarcodeFormat.FormatCode128 - GoogleBarcode.FORMAT_CODE_39 -> BarcodeFormat.FormatCode39 - GoogleBarcode.FORMAT_CODE_93 -> BarcodeFormat.FormatCode93 - GoogleBarcode.FORMAT_DATA_MATRIX -> BarcodeFormat.FormatDataMatrix - GoogleBarcode.FORMAT_EAN_13 -> BarcodeFormat.FormatEAN13 - GoogleBarcode.FORMAT_EAN_8 -> BarcodeFormat.FormatEAN8 - GoogleBarcode.FORMAT_ITF -> BarcodeFormat.FormatITF - GoogleBarcode.FORMAT_PDF417 -> BarcodeFormat.FormatPDF417 - GoogleBarcode.FORMAT_QR_CODE -> BarcodeFormat.FormatQRCode - GoogleBarcode.FORMAT_UPC_A -> BarcodeFormat.FormatUPCA - GoogleBarcode.FORMAT_UPC_E -> BarcodeFormat.FormatUPCE - GoogleBarcode.FORMAT_UNKNOWN -> BarcodeFormat.FormatUnknown - else -> BarcodeFormat.FormatUnknown - } - } - - sealed class BarcodeFormat(val formatName: String) : Parcelable { - @Parcelize - object FormatAztec : BarcodeFormat("aztec") - @Parcelize - object FormatCodaBar : BarcodeFormat("codabar") - @Parcelize - object FormatCode128 : BarcodeFormat("code_128") - @Parcelize - object FormatCode39 : BarcodeFormat("code_39") - @Parcelize - object FormatCode93 : BarcodeFormat("code_93") - @Parcelize - object FormatDataMatrix : BarcodeFormat("data_matrix") - @Parcelize - object FormatEAN13 : BarcodeFormat("ean_13") - @Parcelize - object FormatEAN8 : BarcodeFormat("ean_8") - @Parcelize - object FormatITF : BarcodeFormat("itf") - @Parcelize - object FormatPDF417 : BarcodeFormat("pdf_417") - @Parcelize - object FormatQRCode : BarcodeFormat("qr_code") - @Parcelize - object FormatUPCA : BarcodeFormat("upc_a") - @Parcelize - object FormatUPCE : BarcodeFormat("upc_e") - @Parcelize - object FormatUnknown : BarcodeFormat("unknown") - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleCodeScannerErrorMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleCodeScannerErrorMapper.kt deleted file mode 100644 index 983a00300d7b..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleCodeScannerErrorMapper.kt +++ /dev/null @@ -1,75 +0,0 @@ -package org.wordpress.android.ui.barcodescanner - -import com.google.mlkit.common.MlKitException -import com.google.mlkit.common.MlKitException.ABORTED -import com.google.mlkit.common.MlKitException.ALREADY_EXISTS -import com.google.mlkit.common.MlKitException.CANCELLED -import com.google.mlkit.common.MlKitException.CODE_SCANNER_APP_NAME_UNAVAILABLE -import com.google.mlkit.common.MlKitException.CODE_SCANNER_CAMERA_PERMISSION_NOT_GRANTED -import com.google.mlkit.common.MlKitException.CODE_SCANNER_CANCELLED -import com.google.mlkit.common.MlKitException.CODE_SCANNER_GOOGLE_PLAY_SERVICES_VERSION_TOO_OLD -import com.google.mlkit.common.MlKitException.CODE_SCANNER_PIPELINE_INFERENCE_ERROR -import com.google.mlkit.common.MlKitException.CODE_SCANNER_PIPELINE_INITIALIZATION_ERROR -import com.google.mlkit.common.MlKitException.CODE_SCANNER_TASK_IN_PROGRESS -import com.google.mlkit.common.MlKitException.CODE_SCANNER_UNAVAILABLE -import com.google.mlkit.common.MlKitException.DATA_LOSS -import com.google.mlkit.common.MlKitException.DEADLINE_EXCEEDED -import com.google.mlkit.common.MlKitException.FAILED_PRECONDITION -import com.google.mlkit.common.MlKitException.INTERNAL -import com.google.mlkit.common.MlKitException.INVALID_ARGUMENT -import com.google.mlkit.common.MlKitException.MODEL_HASH_MISMATCH -import com.google.mlkit.common.MlKitException.MODEL_INCOMPATIBLE_WITH_TFLITE -import com.google.mlkit.common.MlKitException.NETWORK_ISSUE -import com.google.mlkit.common.MlKitException.NOT_ENOUGH_SPACE -import com.google.mlkit.common.MlKitException.NOT_FOUND -import com.google.mlkit.common.MlKitException.OUT_OF_RANGE -import com.google.mlkit.common.MlKitException.PERMISSION_DENIED -import com.google.mlkit.common.MlKitException.RESOURCE_EXHAUSTED -import com.google.mlkit.common.MlKitException.UNAUTHENTICATED -import com.google.mlkit.common.MlKitException.UNAVAILABLE -import com.google.mlkit.common.MlKitException.UNIMPLEMENTED -import com.google.mlkit.common.MlKitException.UNKNOWN -import javax.inject.Inject - -class GoogleCodeScannerErrorMapper @Inject constructor() { - @Suppress("ComplexMethod") - fun mapGoogleMLKitScanningErrors( - exception: Throwable? - ): CodeScanningErrorType { - return when ((exception as? MlKitException)?.errorCode) { - ABORTED -> CodeScanningErrorType.Aborted - ALREADY_EXISTS -> CodeScanningErrorType.AlreadyExists - CANCELLED -> CodeScanningErrorType.Cancelled - CODE_SCANNER_APP_NAME_UNAVAILABLE -> CodeScanningErrorType.CodeScannerAppNameUnavailable - CODE_SCANNER_CAMERA_PERMISSION_NOT_GRANTED -> - CodeScanningErrorType.CodeScannerCameraPermissionNotGranted - CODE_SCANNER_CANCELLED -> CodeScanningErrorType.CodeScannerCancelled - CODE_SCANNER_GOOGLE_PLAY_SERVICES_VERSION_TOO_OLD -> - CodeScanningErrorType.CodeScannerGooglePlayServicesVersionTooOld - CODE_SCANNER_PIPELINE_INFERENCE_ERROR -> CodeScanningErrorType.CodeScannerPipelineInferenceError - CODE_SCANNER_PIPELINE_INITIALIZATION_ERROR -> - CodeScanningErrorType.CodeScannerPipelineInitializationError - CODE_SCANNER_TASK_IN_PROGRESS -> CodeScanningErrorType.CodeScannerTaskInProgress - CODE_SCANNER_UNAVAILABLE -> CodeScanningErrorType.CodeScannerUnavailable - DATA_LOSS -> CodeScanningErrorType.DataLoss - DEADLINE_EXCEEDED -> CodeScanningErrorType.DeadlineExceeded - FAILED_PRECONDITION -> CodeScanningErrorType.FailedPrecondition - INTERNAL -> CodeScanningErrorType.Internal - INVALID_ARGUMENT -> CodeScanningErrorType.InvalidArgument - MODEL_HASH_MISMATCH -> CodeScanningErrorType.ModelHashMismatch - MODEL_INCOMPATIBLE_WITH_TFLITE -> CodeScanningErrorType.ModelIncompatibleWithTFLite - NETWORK_ISSUE -> CodeScanningErrorType.NetworkIssue - NOT_ENOUGH_SPACE -> CodeScanningErrorType.NotEnoughSpace - NOT_FOUND -> CodeScanningErrorType.NotFound - OUT_OF_RANGE -> CodeScanningErrorType.OutOfRange - PERMISSION_DENIED -> CodeScanningErrorType.PermissionDenied - RESOURCE_EXHAUSTED -> CodeScanningErrorType.ResourceExhausted - UNAUTHENTICATED -> CodeScanningErrorType.UnAuthenticated - UNAVAILABLE -> CodeScanningErrorType.UnAvailable - UNIMPLEMENTED -> CodeScanningErrorType.UnImplemented - UNKNOWN -> CodeScanningErrorType.Unknown - else -> CodeScanningErrorType.Other(exception) - } - } -} - diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt deleted file mode 100644 index 2e0d339c473e..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/GoogleMLKitCodeScanner.kt +++ /dev/null @@ -1,70 +0,0 @@ -package org.wordpress.android.ui.barcodescanner - -import androidx.camera.core.ImageProxy -import com.google.mlkit.vision.barcode.BarcodeScanner -import com.google.mlkit.vision.barcode.common.Barcode -import kotlinx.coroutines.channels.ProducerScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import javax.inject.Inject - -class GoogleMLKitCodeScanner @Inject constructor( - private val barcodeScanner: BarcodeScanner, - private val errorMapper: GoogleCodeScannerErrorMapper, - private val barcodeFormatMapper: GoogleBarcodeFormatMapper, - private val inputImageProvider: MediaImageProvider, -) : CodeScanner { - private var barcodeFound = false - @androidx.camera.core.ExperimentalGetImage - override fun startScan(imageProxy: ImageProxy): Flow { - return callbackFlow { - val barcodeTask = barcodeScanner.process(inputImageProvider.provideImage(imageProxy)) - barcodeTask.addOnCompleteListener { - // We must call image.close() on received images when finished using them. - // Otherwise, new images may not be received or the camera may stall. - imageProxy.close() - } - barcodeTask.addOnSuccessListener { barcodeList -> - // The check for barcodeFound is done because the startScan method will be called - // continuously by the library as long as we are in the scanning screen. - // There will be a good chance that the same barcode gets identified multiple times and as a result - // success callback will be called multiple times. - if (!barcodeList.isNullOrEmpty() && !barcodeFound) { - barcodeFound = true - handleScanSuccess(barcodeList.firstOrNull()) - this@callbackFlow.close() - } - } - barcodeTask.addOnFailureListener { exception -> - this@callbackFlow.trySend( - CodeScannerStatus.Failure( - error = exception.message, - type = errorMapper.mapGoogleMLKitScanningErrors(exception) - ) - ) - this@callbackFlow.close() - } - - awaitClose() - } - } - - private fun ProducerScope.handleScanSuccess(code: Barcode?) { - code?.rawValue?.let { - trySend( - CodeScannerStatus.Success( - it, - barcodeFormatMapper.mapBarcodeFormat(code.format) - ) - ) - } ?: run { - trySend( - CodeScannerStatus.Failure( - error = "Failed to find a valid raw value!", - type = CodeScanningErrorType.Other(Throwable("Empty raw value")) - ) - ) - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/InputImageProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/InputImageProvider.kt deleted file mode 100644 index 2184e4bcbe7e..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/barcodescanner/InputImageProvider.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.wordpress.android.ui.barcodescanner - -import androidx.camera.core.ImageProxy -import com.google.mlkit.vision.common.InputImage -import javax.inject.Inject - -interface InputImageProvider { - fun provideImage(imageProxy: ImageProxy): InputImage -} -class MediaImageProvider @Inject constructor() : InputImageProvider { - @androidx.camera.core.ExperimentalGetImage - override fun provideImage(imageProxy: ImageProxy): InputImage { - return InputImage.fromMediaImage(imageProxy.image!!, imageProxy.imageInfo.rotationDegrees) - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/BloganuaryNudgeAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/BloganuaryNudgeAnalyticsTracker.kt new file mode 100644 index 000000000000..a02a5db92475 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/BloganuaryNudgeAnalyticsTracker.kt @@ -0,0 +1,50 @@ +package org.wordpress.android.ui.bloganuary + +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.ui.bloganuary.learnmore.BloganuaryNudgeLearnMoreOverlayAction +import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import javax.inject.Inject + +class BloganuaryNudgeAnalyticsTracker @Inject constructor( + private val analyticsTracker: AnalyticsTrackerWrapper, + private val cardsTracker: CardsTracker, +) { + fun trackMySiteCardLearnMoreTapped(isPromptsEnabled: Boolean) = analyticsTracker.track( + Stat.BLOGANUARY_NUDGE_MY_SITE_CARD_LEARN_MORE_TAPPED, + mapOf(PROPERTY_PROMPTS_ENABLED to isPromptsEnabled.toString()) + ) + + fun trackMySiteCardMoreMenuTapped() = cardsTracker.trackCardMoreMenuClicked( + CardsTracker.Type.BLOGANUARY_NUDGE.label + ) + + fun trackMySiteCardMoreMenuItemTapped(item: BloganuaryNudgeCardMenuItem) = cardsTracker + .trackCardMoreMenuItemClicked( + CardsTracker.Type.BLOGANUARY_NUDGE.label, + item.label + ) + + fun trackLearnMoreOverlayShown(isPromptsEnabled: Boolean) = analyticsTracker.track( + Stat.BLOGANUARY_NUDGE_LEARN_MORE_MODAL_SHOWN, + mapOf(PROPERTY_PROMPTS_ENABLED to isPromptsEnabled.toString()) + ) + + fun trackLearnMoreOverlayDismissed() = analyticsTracker.track( + Stat.BLOGANUARY_NUDGE_LEARN_MORE_MODAL_DISMISSED + ) + + fun trackLearnMoreOverlayActionTapped(action: BloganuaryNudgeLearnMoreOverlayAction) = analyticsTracker.track( + Stat.BLOGANUARY_NUDGE_LEARN_MORE_MODAL_ACTION_TAPPED, + mapOf(PROPERTY_ACTION to action.analyticsLabel) + ) + + enum class BloganuaryNudgeCardMenuItem(val label: String) { + HIDE_THIS("hide_this"), + } + + companion object { + private const val PROPERTY_PROMPTS_ENABLED = "prompts_enabled" + private const val PROPERTY_ACTION = "action" + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlay.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlay.kt new file mode 100644 index 000000000000..7e06efaf1d06 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlay.kt @@ -0,0 +1,256 @@ +package org.wordpress.android.ui.bloganuary.learnmore + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeightIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.ContentAlpha +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.LocalContentColor +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +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.ContentAlphaProvider +import org.wordpress.android.ui.compose.theme.AppColor +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.unit.Margin +import org.wordpress.android.ui.compose.utils.uiStringText +import org.wordpress.android.ui.utils.UiString.UiStringRes +import androidx.compose.material.MaterialTheme as Material2Theme + +private val contentIconForegroundColor: Color + get() = AppColor.White + +private val contentIconBackgroundColor: Color + @Composable get() = if (Material2Theme.colors.isLight) { + AppColor.Black + } else { + AppColor.White.copy(alpha = 0.18f) + } + +private val contentTextEmphasis: Float + @Composable get() = if (Material2Theme.colors.isLight) { + 1f + } else { + ContentAlpha.medium + } + +@Composable +fun BloganuaryNudgeLearnMoreOverlay( + model: BloganuaryNudgeLearnMoreOverlayUiState, + onActionClick: (BloganuaryNudgeLearnMoreOverlayAction) -> Unit, + onCloseClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier) { + IconButton( + onClick = onCloseClick, + modifier = Modifier.align(Alignment.End) + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.label_close_button), + ) + } + + Spacer( + Modifier + .requiredHeightIn( + min = Margin.Medium.value, + max = Margin.ExtraExtraMediumLarge.value + ) + ) + + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(horizontal = Margin.ExtraMediumLarge.value) + .padding(bottom = Margin.ExtraLarge.value) + ) { + Image( + painter = painterResource(R.drawable.logo_bloganuary), + colorFilter = ColorFilter.tint(Material2Theme.colors.onSurface), + modifier = Modifier.width(180.dp), + contentScale = ContentScale.Inside, + contentDescription = stringResource( + R.string.bloganuary_dashboard_nudge_overlay_icon_content_description + ) + ) + + Spacer(Modifier.height(Margin.ExtraMediumLarge.value)) + + // Title + Text( + stringResource(R.string.bloganuary_dashboard_nudge_overlay_title), + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ) + + Spacer(Modifier.height(Margin.ExtraExtraMediumLarge.value)) + + // Bullet points + OverlayContent( + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = Margin.ExtraMediumLarge.value), + ) + + // min spacing + Spacer(Modifier.height(Margin.ExtraLarge.value)) + Spacer(Modifier.weight(1f)) + + // Note + Text( + uiStringText(model.noteText), + modifier = Modifier.align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodySmall, + color = LocalContentColor.current.copy(alpha = ContentAlpha.medium), + ) + } + } + + Divider() + + Button( + onClick = { onActionClick(model.action) }, + modifier = Modifier + .fillMaxWidth() + .padding(Margin.ExtraMediumLarge.value), + elevation = null, + contentPadding = PaddingValues(vertical = Margin.Large.value), + colors = ButtonDefaults.buttonColors( + backgroundColor = Material2Theme.colors.onSurface, + contentColor = Material2Theme.colors.surface, + ), + ) { + Text(stringResource(model.action.textRes)) + } + } +} + +@Composable +private fun OverlayContent( + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(Margin.ExtraMediumLarge.value), + modifier = modifier, + ) { + OverlayContentItem( + iconRes = R.drawable.ic_bloganuary_learn_more_item_one, + textRes = R.string.bloganuary_dashboard_nudge_overlay_text_one, + ) + + OverlayContentItem( + iconRes = R.drawable.ic_bloganuary_learn_more_item_two, + textRes = R.string.bloganuary_dashboard_nudge_overlay_text_two, + ) + + OverlayContentItem( + iconRes = R.drawable.ic_bloganuary_learn_more_item_three, + textRes = R.string.bloganuary_dashboard_nudge_overlay_text_three, + ) + } +} + +@Composable +private fun OverlayContentItem( + iconRes: Int, + textRes: Int, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + Box( + modifier = Modifier + .size(48.dp) + .background( + color = contentIconBackgroundColor, + shape = CircleShape, + ), + ) { + Image( + painter = painterResource(iconRes), + contentDescription = null, + colorFilter = ColorFilter.tint(contentIconForegroundColor), + modifier = Modifier + .size(24.dp) + .align(Alignment.Center) + ) + } + + Spacer(Modifier.width(Margin.ExtraLarge.value)) + + ContentAlphaProvider(contentTextEmphasis) { + Text( + stringResource(textRes), + style = MaterialTheme.typography.titleMedium, + ) + } + } +} + + +@Preview(name = "Light Mode") +@Preview(name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun BloganuaryNudgeLearnMoreOverlayPreview() { + AppTheme { + BloganuaryNudgeLearnMoreOverlay( + model = BloganuaryNudgeLearnMoreOverlayUiState( + noteText = UiStringRes( + R.string.bloganuary_dashboard_nudge_overlay_note_prompts_enabled + ), + action = BloganuaryNudgeLearnMoreOverlayAction.DISMISS, + ), + onActionClick = {}, + onCloseClick = {}, + ) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayFragment.kt new file mode 100644 index 000000000000..9454300e4c39 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayFragment.kt @@ -0,0 +1,78 @@ +package org.wordpress.android.ui.bloganuary.learnmore + +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.ui.platform.ComposeView +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.util.extensions.fillScreen +import org.wordpress.android.viewmodel.main.WPMainActivityViewModel +import javax.inject.Inject + +@AndroidEntryPoint +class BloganuaryNudgeLearnMoreOverlayFragment : BottomSheetDialogFragment() { + private val viewModel: BloganuaryNudgeLearnMoreOverlayViewModel by viewModels() + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private val wpMainActivityViewModel by lazy { + ViewModelProvider(requireActivity(), viewModelFactory)[WPMainActivityViewModel::class.java] + } + + private val isPromptsEnabled: Boolean by lazy { + arguments?.getBoolean(ARG_IS_PROMPTS_ENABLED) ?: false + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + (this as? BottomSheetDialog)?.fillScreen(isDraggable = true) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return ComposeView(requireContext()).apply { + setContent { + AppTheme { + BloganuaryNudgeLearnMoreOverlay( + model = viewModel.getUiState(isPromptsEnabled), + onActionClick = viewModel::onActionClick, + onCloseClick = viewModel::onCloseClick, + ) + } + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.dismissDialog.observe(viewLifecycleOwner) { + dismiss() + if (it.refreshDashboard) wpMainActivityViewModel.requestMySiteDashboardRefresh() + } + viewModel.onDialogShown(isPromptsEnabled) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + viewModel.onDialogDismissed() + } + + companion object { + const val TAG = "BloganuaryNudgeLearnMoreOverlayFragment" + private const val ARG_IS_PROMPTS_ENABLED = "isPromptsEnabled" + + fun newInstance(isPromptsEnabled: Boolean) = BloganuaryNudgeLearnMoreOverlayFragment().apply { + arguments = Bundle().apply { + putBoolean(ARG_IS_PROMPTS_ENABLED, isPromptsEnabled) + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayUiState.kt new file mode 100644 index 000000000000..4dc0878504bf --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayUiState.kt @@ -0,0 +1,24 @@ +package org.wordpress.android.ui.bloganuary.learnmore + +import androidx.annotation.StringRes +import org.wordpress.android.R +import org.wordpress.android.ui.utils.UiString + +data class BloganuaryNudgeLearnMoreOverlayUiState( + val noteText: UiString, + val action: BloganuaryNudgeLearnMoreOverlayAction, +) + +enum class BloganuaryNudgeLearnMoreOverlayAction( + @StringRes val textRes: Int, + val analyticsLabel: String, +) { + DISMISS( + textRes = R.string.bloganuary_dashboard_nudge_overlay_action_dismiss, + analyticsLabel = "dismiss", + ), + TURN_ON_PROMPTS( + textRes = R.string.bloganuary_dashboard_nudge_overlay_action_turn_on_prompts, + analyticsLabel = "turn_on_prompts", + ), +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayViewModel.kt new file mode 100644 index 000000000000..5f4e40ff6f1c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayViewModel.kt @@ -0,0 +1,69 @@ +package org.wordpress.android.ui.bloganuary.learnmore + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.wordpress.android.R +import org.wordpress.android.ui.bloganuary.BloganuaryNudgeAnalyticsTracker +import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.viewmodel.SingleLiveEvent +import javax.inject.Inject + +@HiltViewModel +class BloganuaryNudgeLearnMoreOverlayViewModel @Inject constructor( + private val promptsSettingsHelper: BloggingPromptsSettingsHelper, + private val tracker: BloganuaryNudgeAnalyticsTracker, +) : ViewModel() { + data class DismissEvent(val refreshDashboard: Boolean = false) + + private val _dismissDialog = SingleLiveEvent() + val dismissDialog = _dismissDialog as LiveData + + fun getUiState(isPromptsEnabled: Boolean): BloganuaryNudgeLearnMoreOverlayUiState { + val noteText: UiString + val action: BloganuaryNudgeLearnMoreOverlayAction + + if (isPromptsEnabled) { + noteText = UiStringRes(R.string.bloganuary_dashboard_nudge_overlay_note_prompts_enabled) + action = BloganuaryNudgeLearnMoreOverlayAction.DISMISS + } else { + noteText = UiStringRes(R.string.bloganuary_dashboard_nudge_overlay_note_prompts_disabled) + action = BloganuaryNudgeLearnMoreOverlayAction.TURN_ON_PROMPTS + } + + return BloganuaryNudgeLearnMoreOverlayUiState( + noteText = noteText, + action = action, + ) + } + + fun onDialogShown(isPromptsEnabled: Boolean) { + tracker.trackLearnMoreOverlayShown(isPromptsEnabled) + } + + fun onActionClick(action: BloganuaryNudgeLearnMoreOverlayAction) { + tracker.trackLearnMoreOverlayActionTapped(action) + when (action) { + BloganuaryNudgeLearnMoreOverlayAction.DISMISS -> { + _dismissDialog.value = DismissEvent() + } + + BloganuaryNudgeLearnMoreOverlayAction.TURN_ON_PROMPTS -> viewModelScope.launch { + promptsSettingsHelper.updatePromptsCardEnabledForCurrentSite(true) + _dismissDialog.postValue(DismissEvent(refreshDashboard = true)) + } + } + } + + fun onCloseClick() { + _dismissDialog.value = DismissEvent() + } + + fun onDialogDismissed() { + tracker.trackLearnMoreOverlayDismissed() + } +} 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 0600bbcff449..4aef7e8c43dd 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 import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import javax.inject.Inject @@ -16,10 +17,16 @@ class BloggingPromptsPostTagProvider @Inject constructor( fun promptIdSearchReaderTag( tagUrl: String - ): ReaderTag = readerUtilsWrapper.getTagFromTagName( - promptIdTag(tagUrl), - ReaderTagType.FOLLOWED, - ) + ): ReaderTag { + val promptIdTag = promptIdTag(tagUrl) + return ReaderTag( + promptIdTag, + promptIdTag, + promptIdTag, + ReaderPostLogic.formatFullEndpointForTag(promptIdTag), + ReaderTagType.FOLLOWED, + ) + } companion object { const val BLOGGING_PROMPT_TAG = "dailyprompt" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsSettingsHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsSettingsHelper.kt index 32fcd325abb4..b59eeb85d020 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsSettingsHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsSettingsHelper.kt @@ -44,15 +44,20 @@ class BloggingPromptsSettingsHelper @Inject constructor( bloggingRemindersStore.updateBloggingReminders(current.copy(isPromptsCardEnabled = isEnabled)) } + suspend fun updatePromptsCardEnabledForCurrentSite(isEnabled: Boolean) { + val siteId = selectedSiteRepository.getSelectedSite()?.localId()?.value ?: return + val current = bloggingRemindersStore.bloggingRemindersModel(siteId).firstOrNull() ?: return + bloggingRemindersStore.updateBloggingReminders(current.copy(isPromptsCardEnabled = isEnabled)) + } + + fun isPromptsFeatureAvailable(): Boolean { val selectedSite = selectedSiteRepository.getSelectedSite() ?: return false return bloggingPromptsFeature.isEnabled() && selectedSite.isUsingWpComRestApi } suspend fun shouldShowPromptsFeature(): Boolean { - val siteId = selectedSiteRepository.getSelectedSite()?.localId()?.value ?: return false - - return isPromptsFeatureAvailable() && isPromptsSettingEnabled(siteId) && !isPromptSkippedForToday() + return isPromptsFeatureAvailable() && isPromptsSettingEnabled() && !isPromptSkippedForToday() } fun shouldShowPromptsSetting(): Boolean { @@ -66,12 +71,13 @@ class BloggingPromptsSettingsHelper @Inject constructor( return promptSkippedDate != null && isSameDay(promptSkippedDate, Date()) } - private suspend fun isPromptsSettingEnabled( - siteId: Int - ): Boolean = bloggingRemindersStore - .bloggingRemindersModel(siteId) - .firstOrNull() - ?.isPromptsCardEnabled == true + suspend fun isPromptsSettingEnabled(): Boolean { + val siteId = selectedSiteRepository.getSelectedSite()?.localId()?.value ?: return false + return bloggingRemindersStore + .bloggingRemindersModel(siteId) + .firstOrNull() + ?.isPromptsCardEnabled == true + } companion object { private const val TRACK_PROPERTY_ENABLED = "enabled" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/styles/DashboardCardTypography.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/styles/DashboardCardTypography.kt index 7de8fd13db38..e612b56a2e37 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/styles/DashboardCardTypography.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/styles/DashboardCardTypography.kt @@ -28,9 +28,7 @@ object DashboardCardTypography { val detailText: TextStyle @Composable - get() = MaterialTheme.typography.bodyLarge.copy( - fontWeight = FontWeight.Normal, - fontSize = 14.sp, + get() = MaterialTheme.typography.bodyMedium.copy( color = colors.onSurface.copy(alpha = ContentAlpha.medium) ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailContainerFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailContainerFragment.java index dcf919807487..f51a98efabfa 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailContainerFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailContainerFragment.java @@ -30,6 +30,7 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.databinding.HistoryDetailContainerFragmentBinding; import org.wordpress.android.editor.EditorMediaUtils; +import org.wordpress.android.editor.savedinstance.SavedInstanceDatabase; import org.wordpress.android.fluxc.model.revisions.RevisionModel; import org.wordpress.android.fluxc.store.PostStore; import org.wordpress.android.ui.history.HistoryListItem.Revision; @@ -245,7 +246,10 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) { public boolean onOptionsItemSelected(@NonNull MenuItem item) { if (item.getItemId() == R.id.history_load) { Intent intent = new Intent(); - intent.putExtra(KEY_REVISION, mRevision); + SavedInstanceDatabase db = SavedInstanceDatabase.Companion.getDatabase(WordPress.getContext()); + if (db != null) { + db.addParcel(KEY_REVISION, mRevision); + } requireActivity().setResult(Activity.RESULT_OK, intent); requireActivity().finish(); 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 64c8a4072b0c..f5cb5b195fe6 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 @@ -5,9 +5,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import kotlinx.parcelize.parcelableCreator import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.editor.savedinstance.SavedInstanceDatabase.Companion.getDatabase 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,10 +18,10 @@ class HistoryDetailFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mRevision = if (savedInstanceState != null) { - savedInstanceState.getParcelableCompat(KEY_REVISION) + mRevision = if (getDatabase(WordPress.getContext())?.hasParcel(KEY_REVISION) == true) { + getDatabase(WordPress.getContext())?.getParcel(KEY_REVISION, parcelableCreator()) } else { - arguments?.getParcelableCompat(EXTRA_REVISION) + getDatabase(WordPress.getContext())?.getParcel(EXTRA_REVISION, parcelableCreator()) } } @@ -32,7 +34,7 @@ class HistoryDetailFragment : Fragment() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putParcelable(KEY_REVISION, mRevision) + getDatabase(WordPress.getContext())?.addParcel(KEY_REVISION, mRevision) } companion object { @@ -40,11 +42,8 @@ class HistoryDetailFragment : Fragment() { const val KEY_REVISION = "KEY_REVISION" fun newInstance(revision: Revision): HistoryDetailFragment { - val fragment = HistoryDetailFragment() - val bundle = Bundle() - bundle.putParcelable(EXTRA_REVISION, revision) - fragment.arguments = bundle - return fragment + getDatabase(WordPress.getContext())?.addParcel(EXTRA_REVISION, revision) + return HistoryDetailFragment() } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPreviewFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPreviewFragment.kt index acc09f9a4f96..03292ed578f8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPreviewFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPreviewFragment.kt @@ -75,7 +75,7 @@ abstract class LayoutPreviewFragment : FullscreenBottomSheetDialogFragment() { private fun initViewModel() { this.viewModel = getViewModel() - viewModel.previewState.observe(viewLifecycleOwner, { state -> + viewModel.previewState.observe(viewLifecycleOwner) { state -> when (state) { is Loading -> { binding?.desktopPreviewHint?.setVisible(false) @@ -84,6 +84,7 @@ abstract class LayoutPreviewFragment : FullscreenBottomSheetDialogFragment() { binding?.errorView?.setVisible(false) binding?.webView?.loadUrl(state.url) } + is Loaded -> { binding?.progressBar?.setVisible(false) binding?.webView?.setVisible(true) @@ -97,6 +98,7 @@ abstract class LayoutPreviewFragment : FullscreenBottomSheetDialogFragment() { ) AniUtils.animateBottomBar(binding?.desktopPreviewHint, true) } + is Error -> { binding?.progressBar?.setVisible(false) binding?.webView?.setVisible(false) @@ -104,14 +106,14 @@ abstract class LayoutPreviewFragment : FullscreenBottomSheetDialogFragment() { state.toast?.let { ToastUtils.showToast(requireContext(), it) } } } - }) + } // We're skipping the first emitted value since it derives from the view model initialization (`start` method) - viewModel.previewMode.skip(1).observe(viewLifecycleOwner, { load() }) + viewModel.previewMode.skip(1).observe(viewLifecycleOwner) { load() } - viewModel.onPreviewModeButtonPressed.observe(viewLifecycleOwner, { + viewModel.onPreviewModeButtonPressed.observe(viewLifecycleOwner) { previewModeSelectorPopup.show(viewModel) - }) + } binding?.previewTypeSelectorButton?.let { previewModeSelectorPopup = PreviewModeSelectorPopup(requireActivity(), it) @@ -127,7 +129,7 @@ abstract class LayoutPreviewFragment : FullscreenBottomSheetDialogFragment() { binding?.webView?.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) - if (view == null) return + if (!isAdded || view == null) return val width = viewModel.selectedPreviewMode().previewWidth setWebViewWidth(view, width) val widthScript = context?.getString(R.string.web_preview_width_script, width) 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 7c9bba701b21..6453339350c6 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 @@ -8,6 +8,7 @@ import org.wordpress.android.ui.main.utils.MeGravatarLoader import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.ActivityCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BlazeCard.BlazeCampaignsCardModel import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BlazeCard.PromoteWithBlazeCard +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BloganuaryNudgeCardModel import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BloggingPromptCard.BloggingPromptCardWithData import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardPlansCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DomainRegistrationCard @@ -23,6 +24,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.PostCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickLinksItem import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickStartCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.TodaysStatsCard.TodaysStatsCardWithData +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.CategoryEmptyHeaderItem import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.CategoryHeaderItem import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.InfoItem @@ -32,6 +34,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.JetpackBadge import org.wordpress.android.ui.mysite.cards.blaze.BlazeCampaignsCardViewHolder 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.bloganuary.BloganuaryNudgeCardViewHolder 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.domaintransfer.DomainTransferCardViewHolder @@ -49,6 +52,7 @@ import org.wordpress.android.ui.mysite.cards.nocards.NoCardsMessageViewHolder import org.wordpress.android.ui.mysite.cards.personalize.PersonalizeCardViewHolder import org.wordpress.android.ui.mysite.cards.quicklinksitem.QuickLinkRibbonViewHolder import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartCardViewHolder +import org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardViewHolder import org.wordpress.android.ui.mysite.items.categoryheader.MySiteCategoryItemEmptyViewHolder import org.wordpress.android.ui.mysite.items.categoryheader.MySiteCategoryItemViewHolder import org.wordpress.android.ui.mysite.items.infoitem.MySiteInfoItemViewHolder @@ -104,6 +108,7 @@ class MySiteAdapter( learnMoreClicked ) + MySiteCardAndItem.Type.BLOGANUARY_NUDGE_CARD.ordinal -> BloganuaryNudgeCardViewHolder(parent) MySiteCardAndItem.Type.DASHBOARD_DOMAIN_TRANSFER_CARD.ordinal -> DomainTransferCardViewHolder(parent) MySiteCardAndItem.Type.PROMOTE_WITH_BLAZE_CARD.ordinal -> PromoteWithBlazeCardViewHolder(parent, uiHelpers) MySiteCardAndItem.Type.BLAZE_CAMPAIGNS_CARD.ordinal -> BlazeCampaignsCardViewHolder(parent) @@ -120,6 +125,7 @@ class MySiteAdapter( ) MySiteCardAndItem.Type.NO_CARDS_MESSAGE.ordinal -> NoCardsMessageViewHolder(parent) MySiteCardAndItem.Type.PERSONALIZE_CARD.ordinal -> PersonalizeCardViewHolder(parent) + MySiteCardAndItem.Type.WP_SOTW_2023_NUDGE_CARD.ordinal -> WpSotw2023NudgeCardViewHolder(parent) else -> throw IllegalArgumentException("Unexpected view type") } } @@ -140,6 +146,7 @@ class MySiteAdapter( is TodaysStatsCardViewHolder -> holder.bind(getItem(position) as TodaysStatsCardWithData) is PostCardViewHolder<*> -> holder.bind(getItem(position) as PostCard) is BloggingPromptCardViewHolder -> holder.bind(getItem(position) as BloggingPromptCardWithData) + is BloganuaryNudgeCardViewHolder -> holder.bind(getItem(position) as BloganuaryNudgeCardModel) is DomainTransferCardViewHolder -> holder.bind(getItem(position) as DomainTransferCardModel) is PromoteWithBlazeCardViewHolder -> holder.bind(getItem(position) as PromoteWithBlazeCard) is BlazeCampaignsCardViewHolder -> holder.bind(getItem(position) as BlazeCampaignsCardModel) @@ -152,6 +159,7 @@ class MySiteAdapter( is JetpackInstallFullPluginCardViewHolder -> holder.bind(getItem(position) as JetpackInstallFullPluginCard) is NoCardsMessageViewHolder -> holder.bind(getItem(position) as MySiteCardAndItem.Card.NoCardsMessage) is PersonalizeCardViewHolder -> holder.bind(getItem(position) as PersonalizeCardModel) + is WpSotw2023NudgeCardViewHolder -> holder.bind(getItem(position) as WpSotw2023NudgeCardModel) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt index 85c9f678da87..0e7c90394bbd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt @@ -4,6 +4,7 @@ import androidx.recyclerview.widget.DiffUtil import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.ActivityCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BlazeCard.BlazeCampaignsCardModel import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BlazeCard.PromoteWithBlazeCard +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BloganuaryNudgeCardModel import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BloggingPromptCard.BloggingPromptCardWithData import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardPlansCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DomainRegistrationCard @@ -19,6 +20,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.PostCard.PostCardW import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickLinksItem import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickStartCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.TodaysStatsCard.TodaysStatsCardWithData +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.CategoryEmptyHeaderItem import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.CategoryHeaderItem import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.InfoItem @@ -44,6 +46,7 @@ object MySiteAdapterDiffCallback : DiffUtil.ItemCallback() { oldItem is TodaysStatsCardWithData && updatedItem is TodaysStatsCardWithData -> true oldItem is PostCardWithPostItems && updatedItem is PostCardWithPostItems -> true oldItem is BloggingPromptCardWithData && updatedItem is BloggingPromptCardWithData -> true + oldItem is BloganuaryNudgeCardModel && updatedItem is BloganuaryNudgeCardModel -> true oldItem is PromoteWithBlazeCard && updatedItem is PromoteWithBlazeCard -> true oldItem is BlazeCampaignsCardModel && updatedItem is BlazeCampaignsCardModel -> true oldItem is DomainTransferCardModel && updatedItem is DomainTransferCardModel -> true @@ -62,6 +65,7 @@ object MySiteAdapterDiffCallback : DiffUtil.ItemCallback() { oldItem is MySiteCardAndItem.Card.NoCardsMessage && updatedItem is MySiteCardAndItem.Card.NoCardsMessage -> true oldItem is PersonalizeCardModel && updatedItem is PersonalizeCardModel -> true + oldItem is WpSotw2023NudgeCardModel && updatedItem is WpSotw2023NudgeCardModel -> true else -> throw UnsupportedOperationException("Diff not implemented yet") } } 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 1fcec7fe4a6f..aef4980fc62f 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 @@ -44,6 +44,7 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte POST_CARD_ERROR, POST_CARD_WITH_POST_ITEMS, BLOGGING_PROMPT_CARD, + BLOGANUARY_NUDGE_CARD, PROMOTE_WITH_BLAZE_CARD, DASHBOARD_DOMAIN_TRANSFER_CARD, BLAZE_CAMPAIGNS_CARD, @@ -58,6 +59,7 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte JETPACK_INSTALL_FULL_PLUGIN_CARD, NO_CARDS_MESSAGE, PERSONALIZE_CARD, + WP_SOTW_2023_NUDGE_CARD, } data class SiteInfoHeaderCard( @@ -302,6 +304,14 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte ) : BloggingPromptCard(type = Type.BLOGGING_PROMPT_CARD) } + data class BloganuaryNudgeCardModel( + val title: UiString, + val text: UiString, + val onLearnMoreClick: ListItemInteraction, + val onMoreMenuClick: ListItemInteraction, + val onHideMenuItemClick: ListItemInteraction, + ) : Card(Type.BLOGANUARY_NUDGE_CARD) + data class DomainTransferCardModel( @StringRes val title: Int, @StringRes val subtitle: Int, @@ -371,6 +381,14 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte val onMoreMenuClick: ListItemInteraction, ) : Card(type = Type.DASHBOARD_PLANS_CARD) + data class WpSotw2023NudgeCardModel( + val title: UiString, + val text: UiString, + val ctaText: UiString, + val onHideMenuItemClick: ListItemInteraction, + val onCtaClick: ListItemInteraction, + ) : Card(type = Type.WP_SOTW_2023_NUDGE_CARD) + data class NoCardsMessage(val title: UiString, val message: UiString) : Card(Type.NO_CARDS_MESSAGE) data class PersonalizeCardModel(val onClick: () -> Unit) : Card(Type.PERSONALIZE_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 e8b277896a95..034b74826c77 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 @@ -63,6 +63,7 @@ sealed class MySiteCardAndItemBuilderParams { val onErrorRetryClick: () -> Unit, val todaysStatsCardBuilderParams: TodaysStatsCardBuilderParams, val postCardBuilderParams: PostCardBuilderParams, + val bloganuaryNudgeCardBuilderParams: BloganuaryNudgeCardBuilderParams, val bloggingPromptCardBuilderParams: BloggingPromptCardBuilderParams, val domainTransferCardBuilderParams: DomainTransferCardBuilderParams? = null, val blazeCardBuilderParams: BlazeCardBuilderParams? = null, @@ -151,6 +152,13 @@ sealed class MySiteCardAndItemBuilderParams { val onRemoveClick: () -> Unit ) : MySiteCardAndItemBuilderParams() + data class BloganuaryNudgeCardBuilderParams( + val isEligible: Boolean, + val onLearnMoreClick: () -> Unit, + val onMoreMenuClick: () -> Unit, + val onHideMenuItemClick: () -> Unit + ) : MySiteCardAndItemBuilderParams() + sealed class BlazeCardBuilderParams : MySiteCardAndItemBuilderParams() { data class PromoteWithBlazeCardBuilderParams( val onClick: () -> Unit, 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 436397063fd3..3a9d04129a03 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 @@ -31,6 +31,7 @@ import org.wordpress.android.ui.PagePostCreationSourcesDetail import org.wordpress.android.ui.RequestCodes import org.wordpress.android.ui.TextInputDialogFragment import org.wordpress.android.ui.accounts.LoginEpilogueActivity +import org.wordpress.android.ui.bloganuary.learnmore.BloganuaryNudgeLearnMoreOverlayFragment import org.wordpress.android.ui.domains.DomainRegistrationActivity import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayFragment import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil @@ -449,8 +450,11 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment), if (quickStartScrollPosition > 0) recyclerView.scrollToPosition(quickStartScrollPosition) else appbarMain.setExpanded(true) } - } + wpMainActivityViewModel.mySiteDashboardRefreshRequested.observeEvent(viewLifecycleOwner) { + viewModel.refresh() + } + } private fun MySiteFragmentBinding.hideRefreshIndicatorIfNeeded() { swipeRefreshLayout.postDelayed({ @@ -700,7 +704,7 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment), ActivityLauncher.viewCurrentBlogPostsOfType(requireActivity(), action.site, PostListType.SCHEDULED) is SiteNavigationAction.OpenStatsInsights -> ActivityLauncher.viewBlogStatsForTimeframe(requireActivity(), action.site, StatsTimeframe.INSIGHTS) - is SiteNavigationAction.OpenTodaysStatsGetMoreViewsExternalUrl -> + is SiteNavigationAction.OpenExternalUrl -> ActivityLauncher.openUrlExternal(requireActivity(), action.url) is SiteNavigationAction.OpenJetpackPoweredBottomSheet -> showJetpackPoweredBottomSheet() is SiteNavigationAction.OpenJetpackMigrationDeleteWP -> showJetpackMigrationDeleteWP() @@ -751,6 +755,12 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment), is SiteNavigationAction.OpenDashboardPersonalization -> activityNavigator.openDashboardPersonalization( requireActivity() ) + + is SiteNavigationAction.OpenBloganuaryNudgeOverlay -> { + BloganuaryNudgeLearnMoreOverlayFragment + .newInstance(action.isPromptsEnabled) + .show(requireActivity().supportFragmentManager, BloganuaryNudgeLearnMoreOverlayFragment.TAG) + } } private fun handleNavigation(action: BloggingPromptCardNavigationAction) { 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 1bf887a34025..9f6f0fcf4404 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 @@ -63,6 +63,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.activity.ActivityLogCardViewModelSlice +import org.wordpress.android.ui.mysite.cards.dashboard.bloganuary.BloganuaryNudgeCardViewModelSlice import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptCardViewModelSlice import org.wordpress.android.ui.mysite.cards.dashboard.domaintransfer.DomainTransferCardViewModel import org.wordpress.android.ui.mysite.cards.dashboard.pages.PagesCardViewModelSlice @@ -83,6 +84,7 @@ import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository.QuickStartCategory import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardBuilder import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardViewModelSlice +import org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardViewModelSlice import org.wordpress.android.ui.mysite.items.infoitem.MySiteInfoItemBuilder import org.wordpress.android.ui.mysite.items.listitem.SiteItemsBuilder import org.wordpress.android.ui.mysite.items.listitem.SiteItemsViewModelSlice @@ -167,6 +169,8 @@ class MySiteViewModel @Inject constructor( private val noCardsMessageViewModelSlice: NoCardsMessageViewModelSlice, private val siteInfoHeaderCardViewModelSlice: SiteInfoHeaderCardViewModelSlice, private val quickLinksItemViewModelSlice: QuickLinksItemViewModelSlice, + private val bloganuaryNudgeCardViewModelSlice: BloganuaryNudgeCardViewModelSlice, + private val sotw2023NudgeCardViewModelSlice: WpSotw2023NudgeCardViewModelSlice, ) : ScopedViewModel(mainDispatcher) { private val _onSnackbarMessage = MutableLiveData>() private val _onNavigation = MutableLiveData>() @@ -226,9 +230,11 @@ class MySiteViewModel @Inject constructor( activityLogCardViewModelSlice.onNavigation, siteItemsViewModelSlice.onNavigation, bloggingPromptCardViewModelSlice.onNavigation, + bloganuaryNudgeCardViewModelSlice.onNavigation, personalizeCardViewModelSlice.onNavigation, siteInfoHeaderCardViewModelSlice.onNavigation, - quickLinksItemViewModelSlice.navigation + quickLinksItemViewModelSlice.navigation, + sotw2023NudgeCardViewModelSlice.onNavigation, ) val onMediaUpload = siteInfoHeaderCardViewModelSlice.onMediaUpload @@ -242,7 +248,9 @@ class MySiteViewModel @Inject constructor( pagesCardViewModelSlice.refresh, todaysStatsViewModelSlice.refresh, postsCardViewModelSlice.refresh, - activityLogCardViewModelSlice.refresh + activityLogCardViewModelSlice.refresh, + bloganuaryNudgeCardViewModelSlice.refresh, + sotw2023NudgeCardViewModelSlice.refresh, ) val domainTransferCardRefresh = domainTransferCardViewModel.refresh @@ -314,9 +322,11 @@ class MySiteViewModel @Inject constructor( init { dispatcher.register(this) bloggingPromptCardViewModelSlice.initialize(viewModelScope, mySiteSourceManager) + bloganuaryNudgeCardViewModelSlice.initialize(viewModelScope) siteInfoHeaderCardViewModelSlice.initialize(viewModelScope) quickLinksItemViewModelSlice.initialization(viewModelScope) quickLinksItemViewModelSlice.start() + sotw2023NudgeCardViewModelSlice.initialize(viewModelScope) } @Suppress("LongParameterList") @@ -418,8 +428,11 @@ class MySiteViewModel @Inject constructor( val siteItems = getSiteItems(site, activeTask, backupAvailable, scanAvailable) + val sotw2023Card = sotw2023NudgeCardViewModelSlice.buildCard() + return mutableListOf().apply { infoItem?.let { add(infoItem) } + sotw2023Card?.let { add(it) } addAll(siteItems) jetpackSwitchMenu?.let { add(jetpackSwitchMenu) } if (jetpackFeatureCardHelper.shouldShowFeatureCardAtTop()) @@ -475,6 +488,7 @@ class MySiteViewModel @Inject constructor( postCardBuilderParams = postsCardViewModelSlice.getPostsCardBuilderParams( cardsUpdate?.cards?.firstOrNull { it is PostsCardModel } as? PostsCardModel ), + bloganuaryNudgeCardBuilderParams = bloganuaryNudgeCardViewModelSlice.getBuilderParams(), bloggingPromptCardBuilderParams = bloggingPromptCardViewModelSlice.getBuilderParams( bloggingPromptUpdate ), @@ -982,6 +996,8 @@ class MySiteViewModel @Inject constructor( .forEach { personalizeCardViewModelSlice.trackShown(it.type) } siteSelected.dashboardData.filterIsInstance() .forEach { noCardsMessageViewModelSlice.trackShown(it.type) } + siteSelected.dashboardData.filterIsInstance() + .forEach { _ -> sotw2023NudgeCardViewModelSlice.trackShown() } } private fun resetShownTrackers() { @@ -991,6 +1007,7 @@ class MySiteViewModel @Inject constructor( jetpackFeatureCardShownTracker.resetShown() jetpackInstallFullPluginShownTracker.resetShown() personalizeCardViewModelSlice.resetShown() + sotw2023NudgeCardViewModelSlice.resetShown() } // FluxC events 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 845598162436..a9b593bf2ad4 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 @@ -85,7 +85,7 @@ sealed class SiteNavigationAction { data class EditDraftPost(val site: SiteModel, val postId: Int) : SiteNavigationAction() data class EditScheduledPost(val site: SiteModel, val postId: Int) : SiteNavigationAction() data class OpenStatsInsights(val site: SiteModel) : SiteNavigationAction() - data class OpenTodaysStatsGetMoreViewsExternalUrl(val url: String) : SiteNavigationAction() + data class OpenExternalUrl(val url: String) : SiteNavigationAction() object OpenJetpackPoweredBottomSheet : SiteNavigationAction() object OpenJetpackMigrationDeleteWP : SiteNavigationAction() data class OpenJetpackFeatureOverlay(val source: JetpackFeatureCollectionOverlaySource) : SiteNavigationAction() @@ -107,6 +107,8 @@ sealed class SiteNavigationAction { data class OpenDomainTransferPage(val url: String) : SiteNavigationAction() object OpenDashboardPersonalization : SiteNavigationAction() + + data class OpenBloganuaryNudgeOverlay(val isPromptsEnabled: Boolean): SiteNavigationAction() } sealed class BloggingPromptCardNavigationAction: SiteNavigationAction() { 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 8139772de1f8..5be00ccc3ab8 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 @@ -4,6 +4,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardsBuilderParams import org.wordpress.android.ui.mysite.cards.blaze.BlazeCardBuilder import org.wordpress.android.ui.mysite.cards.dashboard.activity.ActivityCardBuilder +import org.wordpress.android.ui.mysite.cards.dashboard.bloganuary.BloganuaryNudgeCardBuilder 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 @@ -16,6 +17,7 @@ import javax.inject.Inject class CardsBuilder @Inject constructor( private val todaysStatsCardBuilder: TodaysStatsCardBuilder, private val postCardBuilder: PostCardBuilder, + private val bloganuaryNudgeCardBuilder: BloganuaryNudgeCardBuilder, private val bloggingPromptCardBuilder: BloggingPromptCardBuilder, private val domainTransferCardBuilder: DomainTransferCardBuilder, private val blazeCardBuilder: BlazeCardBuilder, @@ -29,6 +31,9 @@ class CardsBuilder @Inject constructor( if (dashboardCardsBuilderParams.showErrorCard) { add(createErrorCard(dashboardCardsBuilderParams.onErrorRetryClick)) } else { + bloganuaryNudgeCardBuilder.build(dashboardCardsBuilderParams.bloganuaryNudgeCardBuilderParams) + ?.let { add(it) } + bloggingPromptCardBuilder.build(dashboardCardsBuilderParams.bloggingPromptCardBuilderParams) ?.let { add(it) } 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 c820257999ab..3941f953f31b 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 @@ -72,6 +72,12 @@ class CardsShownTracker @Inject constructor( Type.BLOGGING_PROMPT.label ) ) + is Card.BloganuaryNudgeCardModel -> trackCardShown( + Pair( + card.type.toTypeValue().label, + Type.BLOGANUARY_NUDGE.label + ) + ) is Card.BlazeCard.PromoteWithBlazeCard -> trackCardShown( Pair( card.type.toTypeValue().label, 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 a48cdd9746e0..209d2ad6dd67 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 @@ -23,6 +23,7 @@ class CardsTracker @Inject constructor( STATS("stats"), POST("post"), BLOGGING_PROMPT("blogging_prompt"), + BLOGANUARY_NUDGE("bloganuary_nudge"), PROMOTE_WITH_BLAZE("promote_with_blaze"), BLAZE_CAMPAIGNS("blaze_campaigns"), PAGES("pages"), @@ -133,6 +134,7 @@ fun MySiteCardAndItem.Type.toTypeValue(): Type { MySiteCardAndItem.Type.TODAYS_STATS_CARD -> Type.STATS MySiteCardAndItem.Type.POST_CARD_ERROR -> Type.ERROR MySiteCardAndItem.Type.POST_CARD_WITH_POST_ITEMS -> Type.POST + MySiteCardAndItem.Type.BLOGANUARY_NUDGE_CARD -> Type.BLOGANUARY_NUDGE MySiteCardAndItem.Type.BLOGGING_PROMPT_CARD -> Type.BLOGGING_PROMPT MySiteCardAndItem.Type.PROMOTE_WITH_BLAZE_CARD -> Type.PROMOTE_WITH_BLAZE MySiteCardAndItem.Type.DASHBOARD_DOMAIN_TRANSFER_CARD -> Type.DASHBOARD_CARD_DOMAIN_TRANSFER diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCard.kt new file mode 100644 index 000000000000..daec7574b4a9 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCard.kt @@ -0,0 +1,113 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.bloganuary + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.MaterialTheme +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +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.card.UnelevatedCard +import org.wordpress.android.ui.compose.styles.DashboardCardTypography +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.unit.Margin +import org.wordpress.android.ui.compose.utils.uiStringText +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BloganuaryNudgeCardModel +import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbar +import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbarContextMenuItem +import org.wordpress.android.ui.utils.ListItemInteraction +import org.wordpress.android.ui.utils.UiString + +@Composable +fun BloganuaryNudgeCard( + model: BloganuaryNudgeCardModel, + modifier: Modifier = Modifier, +) { + UnelevatedCard( + modifier = modifier.semantics(mergeDescendants = true) {}, + ) { + Column { + CardToolbar(model) + + Spacer(Modifier.height(Margin.Medium.value)) + + Text( + text = uiStringText(model.title), + style = DashboardCardTypography.subTitle, + modifier = Modifier.padding(horizontal = Margin.ExtraLarge.value), + ) + + Spacer(Modifier.height(Margin.Small.value)) + + Text( + text = uiStringText(model.text), + style = DashboardCardTypography.detailText, + modifier = Modifier.padding(horizontal = Margin.ExtraLarge.value), + ) + + Spacer(Modifier.height(Margin.Small.value)) + + TextButton( + onClick = { model.onLearnMoreClick.click() }, + modifier = Modifier.padding(horizontal = Margin.Small.value) + ) { + Text( + text = stringResource(id = R.string.bloganuary_dashboard_nudge_learn_more), + style = DashboardCardTypography.footerCTA, + ) + } + + Spacer(Modifier.height(Margin.Medium.value)) + } + } +} + +@Composable +private fun CardToolbar( + model: BloganuaryNudgeCardModel, +) { + MySiteCardToolbar( + onContextMenuClick = { model.onMoreMenuClick.click() }, + contextMenuItems = listOf( + MySiteCardToolbarContextMenuItem.Option( + text = stringResource(id = R.string.my_site_dashboard_card_more_menu_hide_card), + onClick = { model.onHideMenuItemClick.click() } + ) + ) + ) { + Icon( + painter = painterResource(R.drawable.ic_bloganuary_24dp), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colors.onSurface + ) + } +} + +@Preview(name = "Light Mode") +@Preview(name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES) +@Composable +fun BloganuaryNudgeCardPreview() { + AppTheme { + BloganuaryNudgeCard( + model = BloganuaryNudgeCardModel( + UiString.UiStringRes(R.string.bloganuary_dashboard_nudge_title), + UiString.UiStringRes(R.string.bloganuary_dashboard_nudge_text), + onLearnMoreClick = ListItemInteraction.create { }, + onMoreMenuClick = ListItemInteraction.create { }, + onHideMenuItemClick = ListItemInteraction.create { }, + ) + ) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardBuilder.kt new file mode 100644 index 000000000000..3ea04f1a06f6 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardBuilder.kt @@ -0,0 +1,24 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.bloganuary + +import org.wordpress.android.R +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BloganuaryNudgeCardModel +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BloganuaryNudgeCardBuilderParams +import org.wordpress.android.ui.utils.ListItemInteraction +import org.wordpress.android.ui.utils.UiString.UiStringRes +import javax.inject.Inject + +class BloganuaryNudgeCardBuilder @Inject constructor() { + fun build(params: BloganuaryNudgeCardBuilderParams): BloganuaryNudgeCardModel? { + return if (params.isEligible) { + BloganuaryNudgeCardModel( + title = UiStringRes(R.string.bloganuary_dashboard_nudge_title), + text = UiStringRes(R.string.bloganuary_dashboard_nudge_text), + onLearnMoreClick = ListItemInteraction.create(params.onLearnMoreClick), + onMoreMenuClick = ListItemInteraction.create(params.onMoreMenuClick), + onHideMenuItemClick = ListItemInteraction.create(params.onHideMenuItemClick), + ) + } else { + null + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardViewHolder.kt new file mode 100644 index 000000000000..d992beef6b50 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardViewHolder.kt @@ -0,0 +1,27 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.bloganuary + +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ViewCompositionStrategy +import org.wordpress.android.databinding.BloganuaryNudgeCardBinding +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BloganuaryNudgeCardModel +import org.wordpress.android.ui.mysite.MySiteCardAndItemViewHolder +import org.wordpress.android.util.extensions.viewBinding + +class BloganuaryNudgeCardViewHolder(parent: ViewGroup) : + MySiteCardAndItemViewHolder(parent.viewBinding(BloganuaryNudgeCardBinding::inflate)) { + fun bind(cardModel: BloganuaryNudgeCardModel) = with(binding.bloganuaryNudgeCard) { + // Dispose of the Composition when the view's LifecycleOwner is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + setContent { + AppTheme { + BloganuaryNudgeCard( + model = cardModel, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardViewModelSlice.kt new file mode 100644 index 000000000000..8b292845fccb --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardViewModelSlice.kt @@ -0,0 +1,80 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.bloganuary + +import android.icu.util.Calendar +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.wordpress.android.ui.bloganuary.BloganuaryNudgeAnalyticsTracker +import org.wordpress.android.ui.bloganuary.BloganuaryNudgeAnalyticsTracker.BloganuaryNudgeCardMenuItem +import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BloganuaryNudgeCardBuilderParams +import org.wordpress.android.ui.mysite.SelectedSiteRepository +import org.wordpress.android.ui.mysite.SiteNavigationAction +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.util.DateTimeUtilsWrapper +import org.wordpress.android.util.config.BloganuaryNudgeFeatureConfig +import org.wordpress.android.viewmodel.Event +import javax.inject.Inject + +class BloganuaryNudgeCardViewModelSlice @Inject constructor( + private val bloganuaryNudgeFeatureConfig: BloganuaryNudgeFeatureConfig, + private val bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper, + private val selectedSiteRepository: SelectedSiteRepository, + private val appPrefsWrapper: AppPrefsWrapper, + private val tracker: BloganuaryNudgeAnalyticsTracker, + private val dateTimeUtilsWrapper: DateTimeUtilsWrapper, +) { + private val _onNavigation = MutableLiveData>() + val onNavigation = _onNavigation as LiveData> + + private val _refresh = MutableLiveData>() + val refresh = _refresh as LiveData> + + private lateinit var scope: CoroutineScope + + fun initialize(scope: CoroutineScope) { + this.scope = scope + } + + fun getBuilderParams(): BloganuaryNudgeCardBuilderParams { + val now = dateTimeUtilsWrapper.getCalendarInstance() + val isEligible = bloganuaryNudgeFeatureConfig.isEnabled() && + now.get(Calendar.MONTH) == Calendar.DECEMBER && + bloggingPromptsSettingsHelper.isPromptsFeatureAvailable() && + !isCardHiddenByUser() + + return BloganuaryNudgeCardBuilderParams( + isEligible = isEligible, + onLearnMoreClick = ::onLearnMoreClick, + onMoreMenuClick = ::onMoreMenuClick, + onHideMenuItemClick = ::onHideMenuItemClick, + ) + } + + private fun isCardHiddenByUser(): Boolean { + val siteId = selectedSiteRepository.getSelectedSite()?.siteId ?: return true + return appPrefsWrapper.getShouldHideBloganuaryNudgeCard(siteId) + } + + private fun onLearnMoreClick() { + scope.launch { + val isPromptsEnabled = bloggingPromptsSettingsHelper.isPromptsSettingEnabled() + tracker.trackMySiteCardLearnMoreTapped(isPromptsEnabled) + _onNavigation.value = Event(SiteNavigationAction.OpenBloganuaryNudgeOverlay(isPromptsEnabled)) + } + } + + private fun onMoreMenuClick() { + tracker.trackMySiteCardMoreMenuTapped() + } + + private fun onHideMenuItemClick() { + tracker.trackMySiteCardMoreMenuItemTapped(BloganuaryNudgeCardMenuItem.HIDE_THIS) + scope.launch { + val siteId = selectedSiteRepository.getSelectedSite()?.siteId ?: return@launch + appPrefsWrapper.setShouldHideBloganuaryNudgeCard(siteId, true) + _refresh.postValue(Event(true)) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt index 0a6b9f64a56d..c1d3f4f34ce8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt @@ -50,7 +50,7 @@ class TodaysStatsViewModelSlice @Inject constructor( _onNavigation.value = Event(SiteNavigationAction.ShowJetpackRemovalStaticPostersView) } else { _onNavigation.value = Event( - SiteNavigationAction.OpenTodaysStatsGetMoreViewsExternalUrl( + SiteNavigationAction.OpenExternalUrl( TodaysStatsCardBuilder.URL_GET_MORE_VIEWS_AND_TRAFFIC ) ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCard.kt new file mode 100644 index 000000000000..350448ad9d03 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCard.kt @@ -0,0 +1,101 @@ +package org.wordpress.android.ui.mysite.cards.sotw2023 + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import org.wordpress.android.R +import org.wordpress.android.ui.compose.components.card.UnelevatedCard +import org.wordpress.android.ui.compose.styles.DashboardCardTypography +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.unit.Margin +import org.wordpress.android.ui.compose.utils.uiStringText +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel +import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbar +import org.wordpress.android.ui.mysite.cards.compose.MySiteCardToolbarContextMenuItem +import org.wordpress.android.ui.utils.ListItemInteraction +import org.wordpress.android.ui.utils.UiString.UiStringRes + +@Composable +fun WpSotw2023NudgeCard( + model: WpSotw2023NudgeCardModel, + modifier: Modifier = Modifier, +) { + UnelevatedCard( + modifier = modifier.semantics(mergeDescendants = true) {}, + ) { + Column( + modifier = Modifier.padding(bottom = Margin.Medium.value) + ) { + CardToolbar(model) + + Spacer(Modifier.height(Margin.Medium.value)) + + Text( + text = uiStringText(model.text), + style = DashboardCardTypography.detailText, + modifier = Modifier.padding(horizontal = Margin.ExtraLarge.value), + ) + + Spacer(Modifier.height(Margin.Small.value)) + + TextButton( + onClick = { model.onCtaClick.click() }, + modifier = Modifier.padding(horizontal = Margin.Small.value) + ) { + Text( + text = uiStringText(model.ctaText), + style = DashboardCardTypography.footerCTA, + ) + } + } + } +} + +@Composable +private fun CardToolbar( + model: WpSotw2023NudgeCardModel +) { + MySiteCardToolbar( + contextMenuItems = listOf( + MySiteCardToolbarContextMenuItem.Option( + text = stringResource(R.string.my_site_dashboard_card_more_menu_hide_card), + onClick = { model.onHideMenuItemClick.click() }, + ) + ), + ) { + Text( + text = uiStringText(uiString = model.title), + style = DashboardCardTypography.smallTitle, + textAlign = TextAlign.Start, + fontWeight = FontWeight.Medium, + ) + } +} + +@Preview(name = "Light Mode") +@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun WpSotw2023NudgeCardPreview() { + AppTheme { + WpSotw2023NudgeCard( + model = WpSotw2023NudgeCardModel( + title = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_title), + text = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_text), + ctaText = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_cta), + onHideMenuItemClick = ListItemInteraction.create {}, + onCtaClick = ListItemInteraction.create {}, + ) + ) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTracker.kt new file mode 100644 index 000000000000..be84de153368 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTracker.kt @@ -0,0 +1,30 @@ +package org.wordpress.android.ui.mysite.cards.sotw2023 + +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import javax.inject.Inject + +class WpSotw2023NudgeCardAnalyticsTracker @Inject constructor( + private val analyticsTracker: AnalyticsTrackerWrapper, +) { + private var cardShownTracked: Boolean = false + + fun resetShown() { + cardShownTracked = false + } + + fun trackShown() { + if (!cardShownTracked) { + cardShownTracked = true + analyticsTracker.track(AnalyticsTracker.Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN) + } + } + + fun trackCtaTapped() { + analyticsTracker.track(AnalyticsTracker.Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_CTA_TAPPED) + } + + fun trackHideTapped() { + analyticsTracker.track(AnalyticsTracker.Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_HIDE_TAPPED) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewHolder.kt new file mode 100644 index 000000000000..e924cd6ff113 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewHolder.kt @@ -0,0 +1,27 @@ +package org.wordpress.android.ui.mysite.cards.sotw2023 + +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ViewCompositionStrategy +import org.wordpress.android.databinding.WpSotw20223NudgeCardBinding +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel +import org.wordpress.android.ui.mysite.MySiteCardAndItemViewHolder +import org.wordpress.android.util.extensions.viewBinding + +class WpSotw2023NudgeCardViewHolder(parent: ViewGroup) : + MySiteCardAndItemViewHolder(parent.viewBinding(WpSotw20223NudgeCardBinding::inflate)) { + fun bind(cardModel: WpSotw2023NudgeCardModel) = with(binding.wpSotw2023NudgeCard) { + // Dispose of the Composition when the view's LifecycleOwner is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + setContent { + AppTheme { + WpSotw2023NudgeCard( + model = cardModel, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSlice.kt new file mode 100644 index 000000000000..c38ed0766037 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSlice.kt @@ -0,0 +1,86 @@ +package org.wordpress.android.ui.mysite.cards.sotw2023 + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineScope +import org.wordpress.android.R +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel +import org.wordpress.android.ui.mysite.SiteNavigationAction +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenExternalUrl +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.ui.utils.ListItemInteraction +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.util.DateTimeUtilsWrapper +import org.wordpress.android.util.LocaleManagerWrapper +import org.wordpress.android.util.config.WpSotw2023NudgeFeatureConfig +import org.wordpress.android.viewmodel.Event +import java.time.Instant +import javax.inject.Inject + +class WpSotw2023NudgeCardViewModelSlice @Inject constructor( + private val featureConfig: WpSotw2023NudgeFeatureConfig, + private val appPrefsWrapper: AppPrefsWrapper, + private val dateTimeUtilsWrapper: DateTimeUtilsWrapper, + private val localeManagerWrapper: LocaleManagerWrapper, + private val tracker: WpSotw2023NudgeCardAnalyticsTracker, +) { + private val _onNavigation = MutableLiveData>() + val onNavigation = _onNavigation as LiveData> + + private val _refresh = MutableLiveData>() + val refresh = _refresh as LiveData> + + private lateinit var scope: CoroutineScope + + fun initialize(scope: CoroutineScope) { + this.scope = scope + } + + fun buildCard(): WpSotw2023NudgeCardModel? = WpSotw2023NudgeCardModel( + title = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_title), + text = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_text), + ctaText = UiStringRes(R.string.wp_sotw_2023_dashboard_nudge_cta), + onHideMenuItemClick = ListItemInteraction.create(::onHideMenuItemClick), + onCtaClick = ListItemInteraction.create(::onCtaClick), + ).takeIf { isEligible() } + + fun trackShown() { + tracker.trackShown() + } + + fun resetShown() { + tracker.resetShown() + } + + private fun onHideMenuItemClick() { + tracker.trackHideTapped() + appPrefsWrapper.setShouldHideSotw2023NudgeCard(true) + _refresh.value = Event(true) + } + + private fun onCtaClick() { + tracker.trackCtaTapped() + _onNavigation.value = Event(OpenExternalUrl(URL)) + } + + private fun isEligible(): Boolean { + val eventTime = Instant.parse(POST_EVENT_START) + val now = dateTimeUtilsWrapper.getInstantNow() + val isDateEligible = now.isAfter(eventTime) + + val currentLanguage = localeManagerWrapper.getLanguage() + val isLanguageEligible = currentLanguage.startsWith(TARGET_LANGUAGE, ignoreCase = true) + + return featureConfig.isEnabled() && + !appPrefsWrapper.getShouldHideSotw2023NudgeCard() && + isDateEligible && + isLanguageEligible + } + + companion object { + private const val URL = "https://wordpress.org/state-of-the-word/" + + "?utm_source=mobile&utm_medium=appnudge&utm_campaign=sotw2023" + private const val POST_EVENT_START = "2023-12-12T00:00:00.00Z" + private const val TARGET_LANGUAGE = "en" + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt index 8731f9b9ee5c..94e60930694d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt @@ -68,6 +68,7 @@ class PersonalizationActivity : AppCompatActivity() { PersonalizationScreen() } } + viewModel.onSelectedSiteMissing.observe(this) { finish() } } @Composable diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationViewModel.kt index 8de056bffb92..3567d5ee11bb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationViewModel.kt @@ -1,5 +1,7 @@ package org.wordpress.android.ui.mysite.personalization +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher @@ -21,15 +23,23 @@ class PersonalizationViewModel @Inject constructor( val uiState = dashboardCardPersonalizationViewModelSlice.uiState val shortcutsState = shortcutsPersonalizationViewModelSlice.uiState + private val _onSelectedSiteMissing = MutableLiveData() + val onSelectedSiteMissing = _onSelectedSiteMissing as LiveData + init { shortcutsPersonalizationViewModelSlice.initialize(viewModelScope) dashboardCardPersonalizationViewModelSlice.initialize(viewModelScope) } fun start() { - val siteId = selectedSiteRepository.getSelectedSite()!!.siteId - dashboardCardPersonalizationViewModelSlice.start(siteId) - shortcutsPersonalizationViewModelSlice.start(selectedSiteRepository.getSelectedSite()!!) + val site = selectedSiteRepository.getSelectedSite() + if (site == null) { + _onSelectedSiteMissing.value = Unit + return + } + + dashboardCardPersonalizationViewModelSlice.start(site.siteId) + shortcutsPersonalizationViewModelSlice.start(site) } fun onCardToggled(cardType: CardType, enabled: Boolean) { 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 417b3d86b900..7bed481c2654 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 @@ -71,6 +71,7 @@ import org.wordpress.android.editor.EditorMediaUtils; import org.wordpress.android.editor.EditorThemeUpdateListener; import org.wordpress.android.editor.ExceptionLogger; +import org.wordpress.android.editor.savedinstance.SavedInstanceDatabase; import org.wordpress.android.editor.gutenberg.DialogVisibility; import org.wordpress.android.editor.gutenberg.GutenbergEditorFragment; import org.wordpress.android.editor.gutenberg.GutenbergPropsBuilder; @@ -710,7 +711,11 @@ public void handleOnBackPressed() { mIsNewPost = savedInstanceState.getBoolean(STATE_KEY_IS_NEW_POST, false); updatePostLoadingAndDialogState(PostLoadingState.fromInt( savedInstanceState.getInt(STATE_KEY_POST_LOADING_STATE, 0))); - mRevision = savedInstanceState.getParcelable(STATE_KEY_REVISION); + + if (getDB() != null) { + mRevision = getDB().getParcel(STATE_KEY_REVISION, Revision.CREATOR); + } + mPostEditorAnalyticsSession = PostEditorAnalyticsSession .fromBundle(savedInstanceState, STATE_KEY_EDITOR_SESSION_DATA, mAnalyticsTrackerWrapper); @@ -1166,7 +1171,10 @@ protected void onSaveInstanceState(Bundle outState) { outState.putBoolean(STATE_KEY_UNDO, mMenuHasUndo); outState.putBoolean(STATE_KEY_REDO, mMenuHasRedo); outState.putSerializable(WordPress.SITE, mSite); - outState.putParcelable(STATE_KEY_REVISION, mRevision); + + if (getDB() != null) { + getDB().addParcel(STATE_KEY_REVISION, mRevision); + } outState.putSerializable(STATE_KEY_EDITOR_SESSION_DATA, mPostEditorAnalyticsSession); mIsConfigChange = true; // don't call sessionData.end() in onDestroy() if this is an Android config change @@ -2373,8 +2381,7 @@ public Fragment getItem(int position) { mIsJetpackSsoEnabled); return GutenbergEditorFragment.newInstance( - "", - "", + WordPress.getContext(), mIsNewPost, gutenbergWebViewAuthorizationData, gutenbergPropsBuilder, @@ -2877,10 +2884,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } break; case RequestCodes.HISTORY_DETAIL: - if (data.hasExtra(KEY_REVISION)) { + if (getDB() != null && getDB().hasParcel(KEY_REVISION)) { mViewPager.setCurrentItem(PAGE_CONTENT); - mRevision = data.getParcelableExtra(KEY_REVISION); + mRevision = getDB().getParcel(KEY_REVISION, Revision.CREATOR); new Handler().postDelayed(this::loadRevision, getResources().getInteger(R.integer.full_screen_dialog_animation_duration)); } @@ -3949,4 +3956,8 @@ public void showJetpackSettings() { public LiveData getSavingInProgressDialogVisibility() { return mViewModel.getSavingInProgressDialogVisibility(); } + + @Nullable private SavedInstanceDatabase getDB() { + return SavedInstanceDatabase.Companion.getDatabase(WordPress.getContext()); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java index 16c26897c68b..cec2c3ea0e08 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java @@ -5,8 +5,11 @@ import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; +import org.wordpress.android.util.AppLog; import org.wordpress.android.util.helpers.MediaFile; +import static org.wordpress.android.util.AppLog.T.MEDIA; + public class ImageBlockProcessor extends BlockProcessor { public ImageBlockProcessor(String localId, MediaFile mediaFile) { super(localId, mediaFile); @@ -34,7 +37,11 @@ public ImageBlockProcessor(String localId, MediaFile mediaFile) { @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { JsonElement id = jsonAttributes.get("id"); if (id != null && !id.isJsonNull() && id.getAsString().equals(mLocalId)) { - jsonAttributes.addProperty("id", Integer.parseInt(mRemoteId)); + try { + jsonAttributes.addProperty("id", Integer.parseInt(mRemoteId)); + } catch (NumberFormatException e) { + AppLog.e(MEDIA, e.getMessage()); + } return true; } 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 7eea6d9aaadb..d39eadbbfb90 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 @@ -206,6 +206,8 @@ public enum DeletablePrefKey implements PrefKey { SHOULD_SHOW_SITE_ITEM_AS_QUICK_LINK_IN_DASHBOARD, SHOULD_SHOW_DEFAULT_QUICK_LINK_IN_DASHBOARD, + SHOULD_HIDE_BLOGANUARY_NUDGE_CARD, + SHOULD_HIDE_SOTW2023_NUDGE_CARD, } /** @@ -1807,4 +1809,25 @@ public static void setShouldShowDefaultQuickLink(final String siteItem, final lo public static Boolean getShouldShowDefaultQuickLink(String siteItem, final long siteId) { return prefs().getBoolean(getShouldShowDefaultQuickLinkKey(siteItem, siteId), true); } + + @NonNull + private static String getSiteIdHideBloganuaryNudgeCardKey(long siteId) { + return DeletablePrefKey.SHOULD_HIDE_BLOGANUARY_NUDGE_CARD.name() + siteId; + } + + public static void setShouldHideBloganuaryNudgeCard(final long siteId, final boolean isHidden) { + prefs().edit().putBoolean(getSiteIdHideBloganuaryNudgeCardKey(siteId), isHidden).apply(); + } + + public static boolean getShouldHideBloganuaryNudgeCard(final long siteId) { + return prefs().getBoolean(getSiteIdHideBloganuaryNudgeCardKey(siteId), false); + } + + public static void setShouldHideSotw2023NudgeCard(boolean isHidden) { + prefs().edit().putBoolean(DeletablePrefKey.SHOULD_HIDE_SOTW2023_NUDGE_CARD.name(), isHidden).apply(); + } + + public static boolean getShouldHideSotw2023NudgeCard() { + return prefs().getBoolean(DeletablePrefKey.SHOULD_HIDE_SOTW2023_NUDGE_CARD.name(), false); + } } 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 50c21033b4cd..190794b1d0d7 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 @@ -412,6 +412,18 @@ class AppPrefsWrapper @Inject constructor() { fun getShouldShowDefaultQuickLink(siteItem: String, siteId: Long): Boolean = AppPrefs.getShouldShowDefaultQuickLink(siteItem, siteId) + fun setShouldHideBloganuaryNudgeCard(siteId: Long, isHidden: Boolean) = + AppPrefs.setShouldHideBloganuaryNudgeCard(siteId, isHidden) + + fun getShouldHideBloganuaryNudgeCard(siteId: Long): Boolean = + AppPrefs.getShouldHideBloganuaryNudgeCard(siteId) + + fun setShouldHideSotw2023NudgeCard(isHidden: Boolean): Unit = + AppPrefs.setShouldHideSotw2023NudgeCard(isHidden) + + fun getShouldHideSotw2023NudgeCard(): Boolean = + AppPrefs.getShouldHideSotw2023NudgeCard() + fun getAllPrefs(): Map = AppPrefs.getAllPrefs() fun setString(prefKey: PrefKey, value: String) { 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 90701d21939d..d006711d1277 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 @@ -13,21 +13,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentResultListener -import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker.Stat -import org.wordpress.android.ui.barcodescanner.BarcodeScanningFragment -import org.wordpress.android.ui.barcodescanner.BarcodeScanningFragment.Companion.KEY_BARCODE_SCANNING_REQUEST -import org.wordpress.android.ui.barcodescanner.BarcodeScanningFragment.Companion.KEY_BARCODE_SCANNING_SCAN_STATUS -import org.wordpress.android.ui.barcodescanner.CodeScannerStatus import org.wordpress.android.ui.compose.components.VerticalScrollBox import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.posts.BasicDialogViewModel @@ -52,18 +46,9 @@ import javax.inject.Inject class QRCodeAuthFragment : Fragment() { @Inject lateinit var uiHelpers: UiHelpers - private val qrCodeAuthViewModel: QRCodeAuthViewModel by viewModels() private val dialogViewModel: BasicDialogViewModel by activityViewModels() - @Suppress("DEPRECATION") - private val resultListener = FragmentResultListener { requestKey, result -> - if (requestKey == KEY_BARCODE_SCANNING_REQUEST) { - val resultValue = result.getParcelable(KEY_BARCODE_SCANNING_SCAN_STATUS) - resultValue?.let { qrCodeAuthViewModel.handleScanningResult(it) } - } - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -75,12 +60,10 @@ class QRCodeAuthFragment : Fragment() { } } } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initBackPressHandler() initViewModel(savedInstanceState) - initScannerResultListener() observeViewModel() } @@ -88,25 +71,15 @@ class QRCodeAuthFragment : Fragment() { qrCodeAuthViewModel.actionEvents.onEach(this::handleActionEvents).launchIn(viewLifecycleOwner.lifecycleScope) dialogViewModel.onInteraction.observeEvent(viewLifecycleOwner, qrCodeAuthViewModel::onDialogInteraction) } - private fun initViewModel(savedInstanceState: Bundle?) { val (uri, isDeepLink) = requireActivity().intent?.extras?.let { val uri = it.getString(DEEP_LINK_URI_KEY, null) val isDeepLink = it.getBoolean(IS_DEEP_LINK_KEY, false) uri to isDeepLink } ?: (null to false) - qrCodeAuthViewModel.start(uri, isDeepLink, savedInstanceState) } - private fun initScannerResultListener() { - requireActivity().supportFragmentManager.setFragmentResultListener( - KEY_BARCODE_SCANNING_REQUEST, - viewLifecycleOwner, - resultListener - ) - } - private fun handleActionEvents(actionEvent: QRCodeAuthActionEvent) { when (actionEvent) { is LaunchDismissDialog -> launchDismissDialog(actionEvent.dialogModel) @@ -114,7 +87,6 @@ class QRCodeAuthFragment : Fragment() { is FinishActivity -> requireActivity().finish() } } - private fun launchDismissDialog(model: QRCodeAuthDialogModel) { dialogViewModel.showDialog( requireActivity().supportFragmentManager, @@ -124,22 +96,17 @@ class QRCodeAuthFragment : Fragment() { getString(model.message), getString(model.positiveButtonLabel), model.negativeButtonLabel?.let { label -> getString(label) }, - model.cancelButtonLabel?.let { label -> getString(label) }, - false + model.cancelButtonLabel?.let { label -> getString(label) } ) ) } private fun launchScanner() { qrCodeAuthViewModel.track(Stat.QRLOGIN_SCANNER_DISPLAYED) - replaceFragment(BarcodeScanningFragment()) - } - - private fun replaceFragment(fragment: Fragment) { - val transaction: FragmentTransaction = requireActivity().supportFragmentManager.beginTransaction() - transaction.replace(R.id.fragment_container, fragment) - transaction.addToBackStack(null) - transaction.commit() + val scanner = GmsBarcodeScanning.getClient(requireContext()) + scanner.startScan() + .addOnSuccessListener { barcode -> qrCodeAuthViewModel.onScanSuccess(barcode.rawValue) } + .addOnFailureListener { qrCodeAuthViewModel.onScanFailure() } } private fun initBackPressHandler() { @@ -147,13 +114,11 @@ class QRCodeAuthFragment : Fragment() { qrCodeAuthViewModel.onBackPressed() } } - override fun onSaveInstanceState(outState: Bundle) { qrCodeAuthViewModel.writeToBundle(outState) super.onSaveInstanceState(outState) } } - @Composable private fun QRCodeAuthScreen(viewModel: QRCodeAuthViewModel = viewModel()) { VerticalScrollBox( @@ -161,7 +126,6 @@ private fun QRCodeAuthScreen(viewModel: QRCodeAuthViewModel = viewModel()) { modifier = Modifier.fillMaxSize() ) { val uiState by viewModel.uiState.collectAsState() - @Suppress("UnnecessaryVariable") // See: https://stackoverflow.com/a/69558316/4129245 when (val state = uiState) { is Content -> ContentState(state) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthViewModel.kt index 8fba57d11a90..c17c5f267e16 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthViewModel.kt @@ -23,9 +23,6 @@ import org.wordpress.android.fluxc.network.rest.wpcom.qrcodeauth.QRCodeAuthError import org.wordpress.android.fluxc.store.qrcodeauth.QRCodeAuthStore import org.wordpress.android.fluxc.store.qrcodeauth.QRCodeAuthStore.QRCodeAuthResult import org.wordpress.android.fluxc.store.qrcodeauth.QRCodeAuthStore.QRCodeAuthValidateResult -import org.wordpress.android.ui.barcodescanner.BarcodeScanningTracker -import org.wordpress.android.ui.barcodescanner.CodeScannerStatus -import org.wordpress.android.ui.barcodescanner.ScanningSource import org.wordpress.android.ui.posts.BasicDialogViewModel.DialogInteraction import org.wordpress.android.ui.posts.BasicDialogViewModel.DialogInteraction.Dismissed import org.wordpress.android.ui.posts.BasicDialogViewModel.DialogInteraction.Negative @@ -55,15 +52,12 @@ class QRCodeAuthViewModel @Inject constructor( private val uiStateMapper: QRCodeAuthUiStateMapper, private val networkUtilsWrapper: NetworkUtilsWrapper, private val validator: QRCodeAuthValidator, - private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, - private val barcodeScanningTracker: BarcodeScanningTracker + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper ) : ViewModel() { private val _actionEvents = Channel(Channel.BUFFERED) val actionEvents = _actionEvents.receiveAsFlow() - private val _uiState = MutableStateFlow(Loading) val uiState: StateFlow = _uiState - private var trackingOrigin: String? = null private var data: String? = null private var token: String? = null @@ -71,13 +65,10 @@ class QRCodeAuthViewModel @Inject constructor( private var browser: String? = null private var lastState: QRCodeAuthUiStateType? = null private var isStarted = false - fun start(uri: String? = null, isDeepLink: Boolean = false, savedInstanceState: Bundle? = null) { if (isStarted) return isStarted = true - extractSavedInstanceStateIfNeeded(savedInstanceState) - if (isDeepLink && savedInstanceState == null) { trackingOrigin = ORIGIN_DEEPLINK process(uri) @@ -95,7 +86,7 @@ class QRCodeAuthViewModel @Inject constructor( location = savedInstanceState.getString(LOCATION_KEY, null) trackingOrigin = savedInstanceState.getString(TRACKING_ORIGIN_KEY, ORIGIN_MENU) lastState = QRCodeAuthUiStateType.fromString(savedInstanceState.getString(LAST_STATE_KEY, null)) - } + } } private fun startOrRestoreUiState() { @@ -109,6 +100,7 @@ class QRCodeAuthViewModel @Inject constructor( this::onAuthenticateCancelClicked ) ) + AUTHENTICATING -> postUiState(uiStateMapper.mapToAuthenticating(location = location, browser = browser)) DONE -> postUiState(uiStateMapper.mapToDone(this::onDismissClicked)) // errors @@ -119,37 +111,25 @@ class QRCodeAuthViewModel @Inject constructor( this::onCancelClicked ) ) + EXPIRED_TOKEN -> postUiState(uiStateMapper.mapToExpired(this::onScanAgainClicked, this::onCancelClicked)) NO_INTERNET -> { postUiState(uiStateMapper.mapToNoInternet(this::onScanAgainClicked, this::onCancelClicked)) } - else -> updateUiStateAndLaunchScanner() - } - } - fun handleScanningResult(status: CodeScannerStatus) { - when (status) { - is CodeScannerStatus.Success -> onScanSuccess(status.code) - is CodeScannerStatus.Failure -> onScanFailure(status) - is CodeScannerStatus.Exit -> onExit() - is CodeScannerStatus.NavigateUp -> onBackPressed() + else -> updateUiStateAndLaunchScanner() } } // https://apps.wordpress.com/get/?campaign=login-qr-code#qr-code-login?token=asdfadsfa&data=asdfasdf fun onScanSuccess(scannedValue: String?) { - barcodeScanningTracker.trackSuccess(ScanningSource.QRCODE_LOGIN) track(Stat.QRLOGIN_SCANNER_SCANNED_CODE) process(scannedValue) } - fun onScanFailure(status: CodeScannerStatus.Failure) { - barcodeScanningTracker.trackScanFailure(ScanningSource.QRCODE_LOGIN, status.type) - postActionEvent(FinishActivity) - } - - private fun onExit() { - track(Stat.QRLOGIN_SCANNER_DISMISSED_CAMERA_PERMISSION_DENIED) + fun onScanFailure() { + // Note: This is a result of the tap on "X" within the scanner view + track(Stat.QRLOGIN_SCANNER_DISMISSED) postActionEvent(FinishActivity) } @@ -191,9 +171,7 @@ class QRCodeAuthViewModel @Inject constructor( private fun process(input: String?) { clearProperties() extractQueryParamsIfValid(input) - track(Stat.QRLOGIN_VERIFY_DISPLAYED) - if (data.isNullOrEmpty() || token.isNullOrEmpty()) { track(QRLOGIN_VERIFY_FAILED, TRACK_INVALID_DATA) postUiState(uiStateMapper.mapToInvalidData(this::onScanAgainClicked, this::onCancelClicked)) @@ -209,7 +187,6 @@ class QRCodeAuthViewModel @Inject constructor( postUiState(uiStateMapper.mapToNoInternet(this::onScanAgainClicked, this::onCancelClicked)) return } - viewModelScope.launch { val result = authStore.validate(data = data, token = token) if (result.isError) { @@ -242,6 +219,7 @@ class QRCodeAuthViewModel @Inject constructor( uiStateMapper.mapToAuthFailed(this::onScanAgainClicked, this::onCancelClicked) } } + GENERIC_ERROR, INVALID_RESPONSE, REST_INVALID_PARAM, @@ -252,7 +230,6 @@ class QRCodeAuthViewModel @Inject constructor( private fun extractQueryParamsIfValid(scannedValue: String?) { if (!validator.isValidUri(scannedValue)) return - val queryParams = validator.extractQueryParams(scannedValue) if (queryParams.containsKey(DATA_KEY) && queryParams.containsKey(TOKEN_KEY)) { this.data = queryParams[DATA_KEY].toString() @@ -266,7 +243,6 @@ class QRCodeAuthViewModel @Inject constructor( postUiState(uiStateMapper.mapToNoInternet(this::onScanAgainClicked, this::onCancelClicked)) return } - viewModelScope.launch { val result = authStore.authenticate(data = data, token = token) if (result.isError) { @@ -287,7 +263,6 @@ class QRCodeAuthViewModel @Inject constructor( } private fun mapAuthenticateSuccessToDoneState() = uiStateMapper.mapToDone(this::onDismissClicked) - private fun updateUiStateAndLaunchScanner() { postUiState(uiStateMapper.mapToScanning()) postActionEvent(LaunchScanner) @@ -307,11 +282,8 @@ class QRCodeAuthViewModel @Inject constructor( fun onDialogInteraction(interaction: DialogInteraction) { when (interaction) { - is Positive -> { - track(Stat.QRLOGIN_SCANNER_DISMISSED) - postActionEvent(FinishActivity) - } - is Negative -> onScanAgainClicked() + is Positive -> postActionEvent(FinishActivity) + is Negative -> Unit is Dismissed -> Unit } } 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 bc0c065cc197..5c48b3900cfc 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 @@ -106,6 +106,8 @@ import org.wordpress.android.ui.reader.discover.ReaderPostCardAction.PrimaryActi import org.wordpress.android.ui.reader.discover.ReaderPostCardActionType import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId import org.wordpress.android.ui.reader.tracker.ReaderTracker +import org.wordpress.android.ui.reader.tracker.ReaderTracker.Companion.SOURCE_POST_DETAIL +import org.wordpress.android.ui.reader.tracker.ReaderTracker.Companion.SOURCE_POST_DETAIL_TOOLBAR import org.wordpress.android.ui.reader.utils.ReaderUtils import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.utils.ReaderVideoUtils @@ -1046,21 +1048,13 @@ class ReaderPostDetailFragment : ViewPagerFragment(), } override fun onPrepareMenu(menu: Menu) { - val isReaderImprovementsEnabled = readerImprovementsFeatureConfig.isEnabled() - val postHasUrl = viewModel.post?.hasUrl() == true val menuBrowse = menu.findItem(R.id.menu_browse) - menuBrowse?.isVisible = if (!isReaderImprovementsEnabled) { - // browse require the post to have a URL (some feed-based posts don't have one) or an intercepted URI - postHasUrl || viewModel.interceptedUri != null - } else { - // in the Reader improvements we are only showing this as a fallback for posts with intercepted URI only - !postHasUrl && viewModel.interceptedUri != null - } - + // browse require the post to have a URL (some feed-based posts don't have one) or an intercepted URI + menuBrowse?.isVisible = postHasUrl || viewModel.interceptedUri != null + // share require the post to have a URL val menuShare = menu.findItem(R.id.menu_share) - // share should not be shown as a TopBar item after Reader improvements (only in the "more" menu) - menuShare?.isVisible = postHasUrl && !isReaderImprovementsEnabled + menuShare?.isVisible = postHasUrl } override fun onMenuItemSelected(menuItem: MenuItem) = when (menuItem.itemId) { @@ -1076,6 +1070,19 @@ class ReaderPostDetailFragment : ViewPagerFragment(), } true } + R.id.menu_share -> { + viewModel.post?.let { + readerTracker.trackBlog( + AnalyticsTracker.Stat.SHARED_ITEM_READER, + it.blogId, + it.feedId, + it.isFollowedByCurrentUser, + SOURCE_POST_DETAIL_TOOLBAR, + ) + } + ReaderActivityLauncher.sharePost(context, viewModel.post) + true + } R.id.menu_more -> { viewModel.onMoreButtonClicked() true diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt index 2fe487fa696c..4a528e534147 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/tracker/ReaderTracker.kt @@ -415,6 +415,7 @@ class ReaderTracker @Inject constructor( const val SOURCE_SITE_PREVIEW = "site_preview" const val SOURCE_TAG_PREVIEW = "tag_preview" const val SOURCE_POST_DETAIL = "post_detail" + const val SOURCE_POST_DETAIL_TOOLBAR = "post_detail_toolbar" const val SOURCE_POST_DETAIL_COMMENT_SNIPPET = "post_detail_comment_snippet" const val SOURCE_COMMENT = "comment" const val SOURCE_USER = "user" 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 1306b6e02b94..a7f69cc2fde0 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 @@ -169,7 +169,13 @@ class SiteCreationMainVM @Inject constructor( } else { siteCreationState = requireNotNull(savedInstanceState.getParcelableCompat(KEY_SITE_CREATION_STATE)) val currentStepIndex = savedInstanceState.getInt(KEY_CURRENT_STEP) - wizardManager.setCurrentStepIndex(currentStepIndex) + try { + wizardManager.setCurrentStepIndex(currentStepIndex) + } catch (e: IllegalStateException) { + // If the current step index is invalid, we reset the wizard + wizardManager.setCurrentStepIndex(0) + AppLog.e(T.THEMES, "Resetting site creation wizard: ${e.message}") + } } isStarted = true } 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 9b8b5261fea3..716ff8017f27 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 @@ -133,7 +133,7 @@ class SiteCreationDomainsViewModel @Inject constructor( val domain = requireNotNull(selectedDomain) { "Create site button should not be visible if a domain is not selected" } - tracker.trackDomainSelected(domain.domainName, currentQuery?.value.orEmpty(), domain.cost) + tracker.trackDomainSelected(domain.domainName, currentQuery?.value.orEmpty(), domain.cost, domain.isFree) _createSiteBtnClicked.value = domain } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt index d33852fc0007..fef68c89ce31 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt @@ -47,6 +47,7 @@ class SiteCreationTracker @Inject constructor( SITE_NAME("site_name"), RECOMMENDED("recommended"), DOMAIN_COST("domain_cost"), + IS_FREE("is_free"), } private var designSelectionSkipped: Boolean = false @@ -62,7 +63,7 @@ class SiteCreationTracker @Inject constructor( tracker.track(AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_DOMAINS_ACCESSED) } - fun trackDomainSelected(chosenDomain: String, searchTerm: String, domainCost: String = "free") { + fun trackDomainSelected(chosenDomain: String, searchTerm: String, domainCost: String = "free", isFree: Boolean) { if(plansInSiteCreationFeatureConfig.isEnabled()) { tracker.track( AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_DOMAINS_SELECTED, @@ -70,6 +71,7 @@ class SiteCreationTracker @Inject constructor( CHOSEN_DOMAIN.key to chosenDomain, SEARCH_TERM.key to searchTerm, PROPERTY.DOMAIN_COST.key to domainCost.lowercase(), // Homogenize data (e.g. "Free" becomes "free") + PROPERTY.IS_FREE.key to isFree, ) ) } else { 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 d5a0186b4426..b1c3505aeceb 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 @@ -410,7 +410,7 @@ private void fetchInstalledThemesIfJetpackSite() { } } - private void activateTheme(String themeId) { + private void activateTheme(@NonNull String themeId) { if (!mSite.isUsingWpComRestApi()) { AppLog.i(T.THEMES, "Theme activation requires a site using WP.com REST API. Aborting request."); return; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserAdapter.java index 900feb389b1b..a4d46b6b4198 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserAdapter.java @@ -170,7 +170,10 @@ public View getView(int position, View convertView, ViewGroup parent) { } @SuppressWarnings("deprecation") - private void configureCardView(ThemeViewHolder themeViewHolder, boolean isCurrent) { + private void configureCardView( + @NonNull ThemeViewHolder themeViewHolder, + boolean isCurrent + ) { if (isCurrent) { ColorStateList color = ContextExtensionsKt.getColorStateListFromAttribute( mContext, @@ -200,10 +203,18 @@ private void configureCardView(ThemeViewHolder themeViewHolder, boolean isCurren } } - private void configureImageView(ThemeViewHolder themeViewHolder, String screenshotURL, final String themeId, - final boolean isCurrent) { - mImageManager.load(themeViewHolder.mImageView, ImageType.THEME, getUrlWithWidth(screenshotURL), - ScaleType.FIT_CENTER); + private void configureImageView( + @NonNull ThemeViewHolder themeViewHolder, + @NonNull String screenshotURL, + @NonNull final String themeId, + final boolean isCurrent + ) { + mImageManager.load( + themeViewHolder.mImageView, + ImageType.THEME, + getUrlWithWidth(screenshotURL), + ScaleType.FIT_CENTER + ); themeViewHolder.mCardView.setOnClickListener(new View.OnClickListener() { @Override @@ -217,7 +228,8 @@ public void onClick(View v) { }); } - private String getUrlWithWidth(String screenshotURL) { + @NonNull + private String getUrlWithWidth(@NonNull String screenshotURL) { if (screenshotURL.contains("?")) { return screenshotURL + "&" + THEME_IMAGE_PARAMETER + mViewWidth; } else { @@ -225,8 +237,12 @@ private String getUrlWithWidth(String screenshotURL) { } } - private void configureImageButton(ThemeViewHolder themeViewHolder, final String themeId, final boolean isPremium, - boolean isCurrent) { + private void configureImageButton( + @NonNull ThemeViewHolder themeViewHolder, + @NonNull final String themeId, + final boolean isPremium, + boolean isCurrent + ) { final PopupMenu popupMenu = new PopupMenu(mContext, themeViewHolder.mImageButton); popupMenu.getMenuInflater().inflate(R.menu.theme_more, popupMenu.getMenu()); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserFragment.kt index 39da031d1bf8..b3fc264ddf07 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserFragment.kt @@ -242,7 +242,7 @@ class ThemeBrowserFragment : Fragment(), AbsListView.RecyclerListener, } } - fun setCurrentThemeId(currentThemeId: String?) { + fun setCurrentThemeId(currentThemeId: String) { this.currentThemeId = currentThemeId refreshView() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeWebActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeWebActivity.java index a8ea5f98baf9..c49fe76d2f01 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeWebActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeWebActivity.java @@ -54,8 +54,12 @@ public static String getSiteLoginUrl(SiteModel site) { return WPWebViewActivity.getSiteLoginUrl(site); } - public static void openTheme(Activity activity, @NonNull SiteModel site, @NonNull ThemeModel theme, - @NonNull ThemeWebActivityType type) { + public static void openTheme( + Activity activity, + @NonNull SiteModel site, + @NonNull ThemeModel theme, + @NonNull ThemeWebActivityType type + ) { String url = getUrl(site, theme, type, !theme.isFree()); if (TextUtils.isEmpty(url)) { ToastUtils.showToast(activity, R.string.could_not_load_theme); @@ -72,7 +76,12 @@ public static void openTheme(Activity activity, @NonNull SiteModel site, @NonNul } } - private static void openWPCOMURL(Activity activity, String url, ThemeModel theme, SiteModel site) { + private static void openWPCOMURL( + Activity activity, + String url, + @NonNull ThemeModel theme, + SiteModel site + ) { if (activity == null) { AppLog.e(AppLog.T.UTILS, "ThemeWebActivity requires a non-null activity"); return; @@ -95,6 +104,7 @@ private static void openWPCOMURL(Activity activity, String url, ThemeModel theme activity.startActivityForResult(intent, ThemeBrowserActivity.ACTIVATE_THEME); } + @Nullable public static String getIdentifierForCustomizer(@NonNull SiteModel site, @NonNull ThemeModel theme) { if (site.isJetpackConnected()) { return theme.getThemeId(); @@ -103,8 +113,13 @@ public static String getIdentifierForCustomizer(@NonNull SiteModel site, @NonNul } } - public static String getUrl(@NonNull SiteModel site, @NonNull ThemeModel theme, @NonNull ThemeWebActivityType type, - boolean isPremium) { + @Nullable + public static String getUrl( + @NonNull SiteModel site, + @NonNull ThemeModel theme, + @NonNull ThemeWebActivityType type, + boolean isPremium + ) { if (theme.isWpComTheme()) { switch (type) { case PREVIEW: @@ -113,10 +128,14 @@ public static String getUrl(@NonNull SiteModel site, @NonNull ThemeModel theme, .format(THEME_URL_PREVIEW, UrlUtils.getHost(site.getUrl()), domain, theme.getThemeId()); case DEMO: String url = theme.getDemoUrl(); - if (url.contains("?")) { - return url + "&" + THEME_URL_DEMO_PARAMETER; + if (url != null) { + if (url.contains("?")) { + return url + "&" + THEME_URL_DEMO_PARAMETER; + } else { + return url + "?" + THEME_URL_DEMO_PARAMETER; + } } else { - return url + "?" + THEME_URL_DEMO_PARAMETER; + return null; } case DETAILS: return String.format(THEME_URL_DETAILS, theme.getThemeId()); @@ -126,7 +145,12 @@ public static String getUrl(@NonNull SiteModel site, @NonNull ThemeModel theme, } else { switch (type) { case PREVIEW: - return site.getAdminUrl() + "customize.php?theme=" + getIdentifierForCustomizer(site, theme); + String identifier = getIdentifierForCustomizer(site, theme); + if (identifier != null) { + return site.getAdminUrl() + "customize.php?theme=" + identifier; + } else { + return null; + } case DEMO: return site.getAdminUrl() + "themes.php?theme=" + theme.getThemeId(); case DETAILS: diff --git a/WordPress/src/main/java/org/wordpress/android/util/DateTimeUtilsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/DateTimeUtilsWrapper.kt index 3615d4358a28..8e0a065963e5 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/DateTimeUtilsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/DateTimeUtilsWrapper.kt @@ -1,5 +1,6 @@ package org.wordpress.android.util +import android.icu.util.Calendar import org.wordpress.android.util.AppLog.T import org.wordpress.android.viewmodel.ContextProvider import java.text.SimpleDateFormat @@ -9,6 +10,7 @@ import java.util.Date import java.util.Locale import javax.inject.Inject import android.text.format.DateUtils +import java.time.Instant class DateTimeUtilsWrapper @Inject constructor( private val localeManagerWrapper: LocaleManagerWrapper, @@ -53,4 +55,10 @@ class DateTimeUtilsWrapper @Inject constructor( return DateUtils.getRelativeTimeSpanString(date.time, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS) .toString() } + + fun getCalendarInstance(): Calendar { + return Calendar.getInstance() + } + + fun getInstantNow(): Instant = Instant.now() } diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/BloganuaryNudgeFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/BloganuaryNudgeFeatureConfig.kt new file mode 100644 index 000000000000..2b249966462e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/BloganuaryNudgeFeatureConfig.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 BLOGANUARY_NUDGE_REMOTE_FIELD = "bloganuary_dashboard_nudge" + +@Feature(BLOGANUARY_NUDGE_REMOTE_FIELD, true) +class BloganuaryNudgeFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.BLOGANUARY_DASHBOARD_NUDGE, + BLOGANUARY_NUDGE_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/InAppReviewsFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/InAppReviewsFeatureConfig.kt new file mode 100644 index 000000000000..9bb5777f063d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/InAppReviewsFeatureConfig.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 IN_APP_REVIEWS_REMOTE_FIELD = "in_app_reviews" + +@Feature(IN_APP_REVIEWS_REMOTE_FIELD, false) +class InAppReviewsFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.IN_APP_REVIEWS, + IN_APP_REVIEWS_REMOTE_FIELD +) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/WpSotw2023NudgeFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/WpSotw2023NudgeFeatureConfig.kt new file mode 100644 index 000000000000..d39086ea2e12 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/WpSotw2023NudgeFeatureConfig.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_SOTW_2023_NUDGE_REMOTE_FIELD = "wp_sotw_2023_nudge" + +@Feature(WP_SOTW_2023_NUDGE_REMOTE_FIELD, true) +class WpSotw2023NudgeFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.BLOGANUARY_DASHBOARD_NUDGE, + WP_SOTW_2023_NUDGE_REMOTE_FIELD +) { + override fun isEnabled(): Boolean { + return super.isEnabled() && !BuildConfig.IS_JETPACK_APP + } +} 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 52a05448354c..c7a4dc027b81 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 @@ -39,15 +39,15 @@ fun BottomSheetDialog.fillScreen(isDraggable: Boolean = false) { MaterialR.id.design_bottom_sheet ) ?: return@setOnShowListener + bottomSheet.layoutParams?.let { layoutParams -> + layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT + bottomSheet.layoutParams = layoutParams + } + 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/image/ImageManager.kt b/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt index 958e338bb272..b7106f7ce1a3 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt @@ -46,6 +46,7 @@ import org.wordpress.android.util.AppLog import org.wordpress.android.util.image.ImageType.VIDEO import java.io.File import java.util.Locale +import java.util.concurrent.ExecutionException import javax.inject.Inject import javax.inject.Singleton @@ -308,11 +309,16 @@ class ImageManager @Inject constructor( */ fun preload(context: Context, design: MShot) { if (!context.isAvailable()) return - GlideApp.with(context) - .downloadOnly() - .load(design) - .submit() - .get() // This makes each call blocking, so subsequent calls can be cancelled if needed. + try { + GlideApp.with(context) + .downloadOnly() + .load(design) + .submit() + .get() // This makes each call blocking, so subsequent calls can be cancelled if needed. + } catch (e: ExecutionException) { + // This is a best effort preload, so we don't want to crash the app if an `ExecutionException` is thrown. + AppLog.e(AppLog.T.UTILS, "Error preloading MShot: $e") + } } /** 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 5e884f229949..9bb2b84343d1 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 @@ -137,6 +137,9 @@ class WPMainActivityViewModel @Inject constructor( private val _showPrivacySettingsWithError = SingleLiveEvent() val showPrivacySettingsWithError: LiveData = _showPrivacySettingsWithError + private val _mySiteDashboardRefreshRequested = MutableLiveData>() + val mySiteDashboardRefreshRequested: LiveData> = _mySiteDashboardRefreshRequested + val onFocusPointVisibilityChange = quickStartRepository.activeTask .mapNullable { getExternalFocusPointInfo(it) } .distinctUntilChanged() @@ -410,6 +413,10 @@ class WPMainActivityViewModel @Inject constructor( _showPrivacySettingsWithError.value = requestedAnalyticsPreference } + fun requestMySiteDashboardRefresh() { + this._mySiteDashboardRefreshRequested.value = Event(Unit) + } + data class FocusPointInfo( val task: QuickStartTask, val isVisible: Boolean diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt index f49b23cdc9a4..3a361e03f105 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt @@ -572,7 +572,7 @@ class PagesViewModel private fun copyPageLink(page: Page, context: Context) { // Get the link to the page - val pageLink = postStore.getPostByLocalPostId(page.localId).link + val pageLink = postStore.getPostByLocalPostId(page.localId)?.link ?: return ActivityLauncher.openShareIntent( context, pageLink, diff --git a/WordPress/src/main/res/drawable/ic_bloganuary_learn_more_item_one.xml b/WordPress/src/main/res/drawable/ic_bloganuary_learn_more_item_one.xml new file mode 100644 index 000000000000..1727be270786 --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_bloganuary_learn_more_item_one.xml @@ -0,0 +1,10 @@ + + + diff --git a/WordPress/src/main/res/drawable/ic_bloganuary_learn_more_item_three.xml b/WordPress/src/main/res/drawable/ic_bloganuary_learn_more_item_three.xml new file mode 100644 index 000000000000..a659c512781b --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_bloganuary_learn_more_item_three.xml @@ -0,0 +1,12 @@ + + + diff --git a/WordPress/src/main/res/drawable/ic_bloganuary_learn_more_item_two.xml b/WordPress/src/main/res/drawable/ic_bloganuary_learn_more_item_two.xml new file mode 100644 index 000000000000..9a9cc64f3dda --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_bloganuary_learn_more_item_two.xml @@ -0,0 +1,9 @@ + + + diff --git a/WordPress/src/main/res/drawable/logo_bloganuary.xml b/WordPress/src/main/res/drawable/logo_bloganuary.xml new file mode 100644 index 000000000000..d583391bf63e --- /dev/null +++ b/WordPress/src/main/res/drawable/logo_bloganuary.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/bloganuary_nudge_card.xml b/WordPress/src/main/res/layout/bloganuary_nudge_card.xml new file mode 100644 index 000000000000..8599b3a9abaa --- /dev/null +++ b/WordPress/src/main/res/layout/bloganuary_nudge_card.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/WordPress/src/main/res/layout/wp_sotw_20223_nudge_card.xml b/WordPress/src/main/res/layout/wp_sotw_20223_nudge_card.xml new file mode 100644 index 000000000000..ee8b5955c2c1 --- /dev/null +++ b/WordPress/src/main/res/layout/wp_sotw_20223_nudge_card.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/WordPress/src/main/res/menu/reader_detail.xml b/WordPress/src/main/res/menu/reader_detail.xml index 628255f59808..3112e6db8993 100644 --- a/WordPress/src/main/res/menu/reader_detail.xml +++ b/WordPress/src/main/res/menu/reader_detail.xml @@ -6,14 +6,12 @@ android:id="@+id/menu_browse" android:icon="@drawable/ic_globe_white_24dp" android:title="@string/view_in_browser" - android:visible="false" app:showAsAction="always" /> + لا، توقف عن الاستخدام + نعم، واصل الاستخدام لهذا السبب، نوصي بتحرير المكوّن باستخدام متصفح الويب لديك. لهذا السبب، نوصي بتحرير المكوّن باستخدام محرر الويب. بدلاً من ذلك، يمكنك تمهيد المحتوى عن طريق فك تجميع المكوّن. @@ -20,10 +22,6 @@ Language: ar قد لا تظهر المكوّنات المتداخلة بشكل أكثر عمقًا من %d من المستويات بشكل صحيح في محرر الهاتف المحمول. لنبدأ حان الوقت لمواصلة رحلتك في ووردبريس.كوم على تطبيق Jetpack. - يؤدي تحسين الصورة إلى تقليل حجم الصور المطلوب رفعها بسرعة.\n\nتم تمكين هذا الخيار افتراضيًا، لكن يمكنك تغييره ضمن إعدادات التطبيق في أي وقت. - هل تريد الاستمرار في تحسين الصور؟ - لا، توقف عن الاستخدام - نعم، واصل الاستخدام مسح البحث مرتفع للغاية يرجى إدخال مفتاح الأمان الخاص بك للاستمرار. @@ -105,6 +103,7 @@ Language: ar تغيير الإعدادات تحديد مزيد لا تتوافر سوى الصور والفيديوهات المُحدَّدة التي منحت حق الوصول إليها. + علامة تبويب تخصيص الصفحة الرئيسية عرض كل الحملات كل النشاط كل الصفحات diff --git a/WordPress/src/main/res/values-de/strings.xml b/WordPress/src/main/res/values-de/strings.xml index 509650994a2f..f433aefbe77e 100644 --- a/WordPress/src/main/res/values-de/strings.xml +++ b/WordPress/src/main/res/values-de/strings.xml @@ -1,11 +1,13 @@ + Nein, deaktivieren + Ja, aktiviert lassen Aus diesem Grund empfehlen wir, den Block in deinem Webbrowser zu bearbeiten. Aus diesem Grund empfehlen wir, den Block im Webeditor zu bearbeiten. Alternativ kannst du die Tiefe des Inhalts reduzieren, indem du die Gruppierung des Blocks aufhebst. @@ -20,10 +22,6 @@ Language: de Blöcke, die tiefer als %d Ebenen verschachtelt sind, werden im mobilen Editor möglicherweise nicht richtig dargestellt. Los geht\'s Es ist an der Zeit, deine WordPress-Reise in der Jetpack-App fortzusetzen. - Die Bildoptimierung schrumpft Bilder für schnelleres Hochladen.\n\nDiese Option ist standardmäßig aktiviert, aber du kannst jederzeit Änderungen in den App-Einstellungen vornehmen. - Sollen Bilder weiterhin optimiert werden? - Nein, deaktivieren - Ja, aktiviert lassen Suche löschen Sehr hoch Bitte gib deinen Sicherheitsschlüssel an, um fortzufahren. diff --git a/WordPress/src/main/res/values-en-rGB/strings.xml b/WordPress/src/main/res/values-en-rGB/strings.xml index 91198ee18947..eef8d290bcf2 100644 --- a/WordPress/src/main/res/values-en-rGB/strings.xml +++ b/WordPress/src/main/res/values-en-rGB/strings.xml @@ -1,104 +1,135 @@ + Let’s go! + Turn on blogging prompts + Bloganuary will use Daily Blogging Prompts to send you topics for the month of January. + Bloganuary will use Daily Blogging Prompts to send you topics for the month of January. You have Blogging Prompts currently disabled. + Read other bloggers’ responses to get inspiration and make new connections. + Publish your response. + Receive a new prompt to inspire you each day. + Join our month-long writing challenge + Bloganuary + For the month of January, blogging prompts will come from Bloganuary – our community challenge to build a blogging habit for the new year. + Bloganuary is coming! + No, turn off + Yes, leave on + For this reason, we recommend editing the block using your web browser. + For this reason, we recommend editing the block using the web editor. + Alternatively, you can flatten the content by ungrouping the block. + Go to settings + Cancel + Grant + You have permanently denied camera permission. It is required in order to scan the barcode. Please enable it from the app settings + Camera permission is required in order to scan the barcode + Grant Camera Permission + Camera permission is required to scan the barcode. + Scan Barcode + Blocks nested deeper than %d levels may not render properly in the mobile editor. + Let\'s go + It\'s time to continue your WordPress journey on the Jetpack app. + Image optimisation shrinks images for faster uploading.\n\nThis option is enabled by default, but you can change it in the app settings at any time. + Keep optimising images? + Clear search + Very High Please provide your security key to continue. There was some trouble with the Security key login Use a security key - %s / year - %s for the first year - All Couldn\'t retrieve domains - Couldn\'t retrieve your domains - Error - From <b>Bloganuary</b> + %s for the first year + %s / year + Transfer domain Looking to transfer a domain you already own? - OK + Type to get more suggestions Search for a domain - Site Domain + OK Something went wrong while adding the domain to the basket. Make sure you are online and retry. - Transfer domain - Type to get more suggestions + Error + All + Couldn\'t retrieve your domains + Site Domain + From <b>Bloganuary</b> Edited Filter by author \'%s\' block converted to blocks - *A free domain for one year is included with all paid annual plans. - Active Shortcuts - Add or Remove shortcuts Alternatively, you can convert the content to blocks. - Cards - Choose Site + Add or Remove shortcuts Inactive Shortcuts + Active Shortcuts Shortcuts + Cards + *A free domain for one year is included with all paid annual plans. + Choose Site Use with a site you already started. - Add a site later. - All Domains - Check that you\'re online and pull to refresh. - Choose how to\nuse your domain - Choose how to use your domain - Don\'t worry, you can easily add a site later. Existing WordPress.com site - Expires %1$s - Find a domain Get Domain + Add a site later. Just buy a domain - Open domain details - Purchase a domain + Don\'t worry, you can easily add a site later. + Choose how to\nuse your domain + Choose how to use your domain Purchase Domain - Search your domains + Find a domain Tap below to find your perfect domain. You don\'t have any domains + Check that you\'re online and pull to refresh. + Open domain details + Search your domains + Purchase a domain + All Domains + Expires %1$s Account and Settings - Free for the first year with annual paid plans Select a plan + Free for the first year with annual paid plans 1 follower - Save Saved + Save You might like - Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor. - Deeply nested block - Synced patterns - Tap here to show more details. Ungroup block + Tap here to show more details. + Synced patterns + Deeply nested block + Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor. Block cannot be rendered because it is deeply nested. Tap here for more details. Uh oh, something went wrong. Please try again later. Go to web Tap the personalise button to show more cards. All cards are hidden - Blogging prompts - Cards may show different content depending on what\'s happening with your site - Daily ideas for your blog posts. - Draft posts Learn how to make the most of your site with the app. + Recent actions taken on your site. Overview of your site pages. + Daily ideas for your blog posts. + Blogging prompts Promote a post and see current campaigns. - Recent actions taken on your site. + Your upcoming scheduled posts. Scheduled posts - Views, Visitors and likes Your recent draft posts. - Your upcoming scheduled posts. - Personalise home tab + Draft posts + Views, Visitors and likes + Cards may show different content depending on what\'s happening with your site Add or hide Cards - Change Settings - Only selected photos and videos you’ve given access to are available. + Personalise home tab + Tap to personalise your home tab Personalise your home tab + Change Settings Select More - Tap to personalise your home tab - Add image or video + Only selected photos and videos you’ve given access to are available. + View all campaigns All activity All pages Choose a file - View all campaigns - View all drafts + Add image or video View all scheduled posts - Hi there, I\'m the Jetpack AI Assistant. + View all drafts + View stats Hide this If I can\'t answer your question, I\'ll help you open a support ticket with our team! - View stats + Hi there, I\'m the Jetpack AI Assistant. Access this Paywall block on your web browser for advanced settings. Answer: Question: @@ -106,22 +137,18 @@ Language: en_GB Error submitting support ticket Ticket created Creating support ticket… - What can I help you with? - Send a message… How can I use my custom domain in the app? I forgot my login information + Why can\'t I log in? I can\'t upload photos/videos Help, my site is down! What is my site address? Not sure what to ask? Contact support - Why can\'t I log in? + What can I help you with? + Send a message… Clear %1$d social shares remaining - Connect accounts - Social sharing - Social sharing - Social CLOSE The app WordPress is missing required components and must be reinstalled from the Google Play Store. Installation failed @@ -132,6 +159,10 @@ Language: en_GB As you may know, Google Domains has been sold to Squarespace. Transfer your domains to WordPress.com now, and we\'ll pay all transfer fees plus an extra year of your domain registration. Reclaim Your Google Domains News + Connect accounts + Social sharing + Social sharing + Social Sharing to %1$d accounts Sharing to %1$d of %2$d accounts Sharing to %1$s @@ -140,19 +171,10 @@ Language: en_GB Customise the message Not now Connect accounts - Insert Audio Block Insert Video Block Insert Image Block Insert Gallery Block - Blaze Campaign - The blaze promote flow couldn\'t be loaded - Create Campaign - Increase your traffic by auto-sharing your posts with your friends on social media. - Close editor - Redo last change - Undo last change - 1 social share remaining - Subscribe to share more + Insert Audio Block Create You have not created any campaigns yet. Click create to get started. You have no campaigns @@ -167,6 +189,15 @@ Language: en_GB REJECTED COMPLETED ACTIVE + Create Campaign + Blaze Campaign + The blaze promote flow couldn\'t be loaded + Increase your traffic by auto-sharing your posts with your friends on social media. + Close editor + Redo last change + Undo last change + 1 social share remaining + Subscribe to share more Increase your traffic by auto-sharing your posts with your friends on social media. Social Sharing %s detached @@ -182,311 +213,312 @@ Language: en_GB Your privacy is critically important to us and always has been. We use, store, and process your personal data to optimise our app (and your experience) in various ways. Some uses of your data we absolutely need in order to make things work, and others you can customise from your Settings. Me. Manage your profile details. Message - Account closed. - All ready to go! - An error occurred while closing account. - Block grouped Block ungrouped - Free domain with annual plan - Get a free domain for the first year, remove ads on your site, and increase your storage. - Homepage + Block grouped It may take up to 30 minutes for your domain to start working properly. + Your new domain <b>%s</b> is being set up. + All ready to go! + Get a free domain for the first year, remove ads on your site, and increase your storage. + Free domain with annual plan Learn more about templates + Your homepage is using a Theme template and will open in the web editor. + Homepage + Account closed. + An error occurred while closing account. This user account cannot be closed while it has active purchases. This user account cannot be closed while it has active subscriptions. - Your homepage is using a Theme template and will open in the web editor. - Your new domain <b>%s</b> is being set up. - Confirm Close Account… This user account cannot be closed if there are unresolved chargebacks. This user account cannot be closed immediately because it has active purchases. Please contact our support team to finish deleting the account. You\'re not authorised to close the account. Couldn\'t close account automatically! - Close Account + Confirm Close Account… To confirm, please re-enter your username before closing. + Close Account Find out more - Twitter auto-sharing is no longer available Twitter auto-sharing is no longer available due to Twitter\'s changes in terms and pricing. + Twitter auto-sharing is no longer available Editing reusable blocks is not yet supported on %s for iOS - Allow notifications to keep up with your site Editing reusable blocks is not yet supported on %s for Android + Allow notifications to keep up with your site The Jetpack app has all the WordPress app’s functionality, and now exclusive access to Stats, Reader, Notifications and more. - Use WordPress with %s in the Jetpack\u00A0app. Use WordPress with %s in the Jetpack\u00A0app. - Like the example above, a domain allows people to find and visit your site from their web browser. - Recent activity + Use WordPress with %s in the Jetpack\u00A0app. Unlabeled colour. %s + Recent activity + Like the example above, a domain allows people to find and visit your site from their web browser. YourSiteName.com + Search with keywords + Search for a short and memorable domain to help people find and visit your site. for the first year + Your website has been created successfully, but we encountered an issue while preparing your custom domain for checkout. Please try again or contact support for assistance. It may take up to 30 minutes for your custom domain to start working. - Search for a short and memorable domain to help people find and visit your site. - Search with keywords We’ve emailed your receipt. %s - Your website has been created successfully, but we encountered an issue while preparing your custom domain for checkout. Please try again or contact support for assistance. App notifications have been disabled. Tap here to enable them. + We recommend <b>uninstalling the WordPress app</b> on your device to avoid data conflicts. It looks like you still have the WordPress app installed. + You no longer need the WordPress app on your device We recommend <b>uninstalling the WordPress app</b> on your device to avoid data conflicts. - We recommend <b>uninstalling the WordPress app</b> on your device to avoid data conflicts. Welcome to the Jetpack app. You can uninstall the WordPress app. - You no longer need the WordPress app on your device - Privacy and Rating Remove blocks - Add Pages to your site - Create Another Page - Describe the purpose of the image. Leave empty if decorative. - Dynamic - Manual - Playback Bar Colour + Privacy and Rating Playback Settings + Playback Bar Colour + Manual + Dynamic + Describe the purpose of the image. Leave empty if decorative. Start with bespoke, mobile-friendly layouts + Create Another Page + Add Pages to your site To use blogging reminders, you\'ll need to turn on push notifications. - %s needs permission to access your audios - %s needs permission to access your music, audio, photos and videos - %s needs permission to access your photos - %s needs permission to access your photos and videos - %s needs permission to access your videos + Turn on push notifications Continue with subdomain + Purchase domain + Photos and videos & Music and audio Music and audio Photos and videos - Photos and videos & Music and audio - Purchase domain + %s needs permission to access your audios + %s needs permission to access your videos + %s needs permission to access your photos + %s needs permission to access your photos and videos + %s needs permission to access your music, audio, photos and videos Turn on notifications - Turn on push notifications Go to Settings → Notifications → App Settings, and turn %1$s on to be notified immediately. - Dismiss notification permission warning. - Fix + You\'ll need to open the app to see notifications. Push notifications are turned off Push notifications are turned off. - You\'ll need to open the app to see notifications. + Dismiss notification permission warning. + Fix <b>%1$s</b> is using %2$s individual Jetpack plugins <b>%1$s</b> is using the <b>%2$s</b> plugin Sites with individual Jetpack plugins aren’t supported by the WordPress app. <b>%1$s</b> is using individual Jetpack plugins, which aren’t supported by the WordPress app. <b>%1$s</b> is using the <b>%2$s</b> plugin, which isn’t supported by the WordPress 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 + Unable to access one of your sites + Please switch to the Jetpack app where we’ll guide you through connecting the full Jetpack plugin to use this site with the app. Switch to the Jetpack app %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. Please install the %3$s. This site + %1$s is using %2$s, which don’t support all features of the app yet. Please install the %3$s. %1$s is using %2$s, which doesn’t support all features of the app yet. Please install the %3$s. Moving to the Jetpack app in a few days. Switching is free and only takes a minute. - Content - Done Learn more at Jetpack.com - Manage - Now that Jetpack is installed, we just need to get you set up. This will only take a minute. - Set up Switch to the Jetpack app - Traffic WP Admin + Manage + Traffic + Content + Set up + Done + Now that Jetpack is installed, we just need to get you set up. This will only take a minute. 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 + Your content will appear on millions of WordPress and Tumblr sites. Promote any post or page in only a few minutes for just a few dollars a day. - Recommended + Drive more traffic to your site with Blaze + Blaze + This domain is already registered Sale + Recommended + Best Alternative + per year + Help See our FAQ for answers to common questions you may have. Thank you for switching to the Jetpack app! - This domain is already registered + Logs Tickets - Your content will appear on millions of WordPress and Tumblr sites. - per year + Free + Help Blocks menu - By setting up Jetpack you agree to our + Display your work across millions of sites. + Promote your content with Blaze Close Contact support - Display your work across millions of sites. Install the full plugin - Promote your content with Blaze Terms and conditions + By setting up Jetpack you agree to our full Jetpack plugin individual Jetpack plugins the %1$s plugin %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. - Only one site is available, so you can\'t change your primary site. Please install the full Jetpack plugin + Only one site is available, so you can\'t change your primary site. Contact Support - Error icon - Jetpack could not be installed at this time. Retry + Jetpack could not be installed at this time. There was a problem - Promote with Blaze + Error icon + Ready to use this site with the app. + Jetpack installed + Installing Jetpack on your site. This can take up to a few minutes to complete. + Installing Jetpack Continue + Your website credentials will not be stored and are used only for the purpose of installing Jetpack. Install Jetpack - Installing Jetpack - Installing Jetpack on your site. This can take up to a few minutes to complete. Jetpack icon - Jetpack installed - Ready to use this site with the app. - Your website credentials will not be stored and are used only for the purpose of installing Jetpack. + Promote with Blaze Unlock your site’s full potential. Get stats, notifications and more with Jetpack. Your site has the Jetpack plugin + 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. 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 + Jetpack lets you do more with your WordPress site. Switching is free and only takes a minute. 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. + Notification will include a word or short phrase for inspiration + Visit <b>Site Settings</b> to turn back on + Blogging Prompts hidden Turn off prompts Get help from our group of volunteers. Community forums Blogging reminders Show prompts Blogging + Please install Google Play Store to get the Jetpack app + Do this later + Switch to Jetpack + Stats, Reader, Notifications and other Jetpack powered features have been removed from the WordPress app. + Jetpack features have moved. %1$s are moving in %2$s - %1$s are moving soon %1$s is moving in %2$s + %1$s are moving soon %1$s is moving soon - Do this later - Jetpack features have moved. - 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. - Switch to Jetpack - %1$s higher than the previous 7 days - %1$s lower than the previous 7 days - %d weeks - 1 week Get the Jetpack app - Last 7 days - Previous 7 days View all responses - Your views in the last 7 days are %1$s higher than the previous 7 days. - Your views in the last 7 days are %1$s lower than the previous 7 days. - Your visitors in the last 7 days are %1$s higher than the previous 7 days. + %1$s lower than the previous 7 days + %1$s higher than the previous 7 days Your visitors in the last 7 days are %1$s lower than the previous 7 days. + Your visitors in the last 7 days are %1$s higher than the previous 7 days. + Your views in the last 7 days are %1$s lower than the previous 7 days. + Your views in the last 7 days are %1$s higher than the previous 7 days. + Previous 7 days + Last 7 days + %d weeks + 1 week From <b>DayOne</b> - Learn more at jetpack.com Remind me later - Stats, Reader, Notifications and other Jetpack-powered features will be removed from the WordPress app on %s. - Stats, Reader, Notifications and other Jetpack-powered features will be removed from the WordPress app soon. + Stats, Reader, Notifications and other features will soon move to the Jetpack mobile app. Switch to the Jetpack app + Learn more at jetpack.com 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 - Check your network connection and try again. + Stats, Reader, Notifications and other Jetpack-powered features will be removed from the WordPress app soon. + Stats, Reader, Notifications and other Jetpack-powered features will be removed from the WordPress app on %s. Jetpack features are moving soon. - No prompts yet Notifications are moving to Jetpack - Oops Reader is moving to the Jetpack app + Your stats are moving to the Jetpack app Switch to the new Jetpack app - There was an error loading prompts. + Check your network connection and try again. Unable to load this content right now - Your stats are moving to the Jetpack app + There was an error loading prompts. + Oops + No prompts yet + %d answers + 1 answer + 0 answers ✓ Answered - close Prompts + close + Alternatively, you can detach and edit this block separately by tapping “Detach”. + Permanently delete \'%s\' Category? Category deleted successfully Deleting category failed - Permanently delete \'%s\' Category? Deleting category - Update category Updating category - Block user + Update category Posts from this user will no longer be shown + Block user Report this user - Continue without Jetpack - Create a new WordPress site with the Jetpack app + Open links in WordPress It looks like you have the Jetpack app installed.\n\nWould you like to open links in the Jetpack app in the future?\n\nYou can always change this in App Settings > Open links in Jetpack + Open links in Jetpack? + Continue without Jetpack Jetpack provides stats, notifications and more to help you build and grow the WordPress site of your dreams.\n\nThe WordPress app no longer supports creating a new site. Jetpack provides stats, notifications and more to help you build and grow the WordPress site of your dreams. - Open links in Jetpack? - Open links in WordPress - Switch to the Jetpack app to keep receiving realtime notifications on your device. - urilinks + Create a new WordPress site with the Jetpack app weblinks + urilinks + Switch to the Jetpack app to keep receiving realtime notifications on your device. Switch to the Jetpack app to find, follow, and like all your favourite sites and posts with Reader. Switch to the Jetpack app to watch your site’s traffic grow with stats and insights. - Follow any site with the Jetpack app Get your notifications with the Jetpack app + Follow any site with the Jetpack app Get your stats using the new Jetpack app - Got it - Need help? - Open links in Jetpack Unable to disable open links in Jetpack Unable to enable open links in Jetpack + Open links in Jetpack + Need help? + Got it + 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. - 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. + 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. + Uh oh, something went wrong.. + Try again Finish Remove WordPress App icon - Try again - Uh oh, something went wrong.. 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. You’ll get all the same notifications but now they’ll come from the Jetpack app. + WordPress help centre + Support Allows the app to disable WordPress notifications. disable WordPress notifications - Support - WordPress help centre - Continue - It looks like you’re switching from the WordPress app. Need help? + Continue We found your site. Continue to transfer all your data and sign in to Jetpack automatically. We found your sites. Continue to transfer all your data and sign in to Jetpack automatically. Your profile photo + It looks like you’re switching from the WordPress app. Welcome to Jetpack! icon - Page Attributes Parent Page + Page Attributes Contribute News 1 answer WordPress icon Write, edit, and publish from anywhere. - Author Couldn\'t retrieve authors + Author Enjoying %s? - Jetpack Social Connections + Share post to %s Jetpack Social Connections Please log in to the Jetpack app to add a widget. - Share post to %s - Check your email on this device! + Jetpack Social Connections We just sent a magic link to - Find, follow, and like all your favourite sites and posts with Reader, now available in the new Jetpack app. - Notifications are powered by Jetpack - Reader is powered by Jetpack - Stats are powered by Jetpack - Stay informed with realtime updates for new comments, site traffic, security reports and more. + Check your email on this device! Use password to sign in + Stay informed with realtime updates for new comments, site traffic, security reports and more. + Notifications are powered by Jetpack Watch your traffic grow and learn about your audience with redesigned Stats and Insights, now available in the new Jetpack app. + Stats are powered by Jetpack + Find, follow, and like all your favourite sites and posts with Reader, now available in the new Jetpack app. + Reader is powered by Jetpack The new Jetpack app has Stats, Reader, Notifications, and more that make your WordPress better. WordPress is better with Jetpack + Upgrade your plan to use video covers + Upgrade your plan to upload audio + Jetpack powered + Invalid URL. + Gradient Continue to Notifications - Continue to Reader Continue to Stats - Gradient - Invalid URL. - Jetpack powered - Upgrade your plan to upload audio - Upgrade your plan to use video covers + Continue to Reader Try the new Jetpack app Problem displaying block. \nTap to attempt block recovery. - ⭐️ Your latest post %1$s has received %2$s like. Last week, you had %1$s views and %2$s comments Last week, you had %1$s views and %2$s likes Last week, you had %1$s views. Last week, you had %1$s views, %2$s likes, and %3$s comments. + ⭐️ Your latest post %1$s has received %2$s like. Jetpack powered Image indicating scan log-in code in process Image indicating an error @@ -546,8 +578,8 @@ Language: en_GB Scan Log-in Code ⭐️ Your latest post %1$s has received %2$s likes. Not enough activity. Check back later when your site\'s had more visitors! - %1$s (%2$s%%) %1$s, %2$s%% of total followers + %1$s (%2$s%%) Copy link Congrats! You know your way around<br/> Get to know the app @@ -612,11 +644,6 @@ Language: en_GB Replace current featured image? Dismiss We’ll be removing the Classic Editor for new posts soon, but this won’t affect editing any of your existing posts or pages. Get a head start by enabling the Block Editor now in site settings. - No - Yes - Cancel - OK - http(s):// Try the new Block Editor Edit %s block Saving @@ -624,8 +651,11 @@ Language: en_GB Remove upload Retry Couldn\'t upload file - OK - Please wait until all files have been saved + No + Yes + Cancel + OK + http(s):// Insert link Beta Editor is still loading @@ -636,6 +666,8 @@ Language: en_GB Pick a media from gallery Take Photo or Video with camera %dpx + OK + Please wait until all files have been saved Files saving Content Cast the movie of your life. @@ -644,16 +676,24 @@ Language: en_GB Note: We’ll show you a new prompt each day on your dashboard to help get those creative juices flowing! 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! + Introducing\nBlogging Prompts Set reminders Include a Blogging Prompt - Introducing\nBlogging Prompts + Posting regularly attracts new readers. Tell us when you want to write and we’ll send you a reminder! Become a better writer by building a habit Writing & Poetry Travel Technology Sports Real Estate + Politics + Photography + Personal + People + Parenting + News + Music + Local Services Lifestyle Interior Design Health @@ -666,226 +706,218 @@ Language: en_GB DIY Education Community & Non-Profit - Politics - Photography - Personal - People - Parenting - News - Music - Local Services - Automotive - Art - Site topic - Tap <b>%1$s</b> to continue. - View more prompts Business Books Beauty + Automotive + Art E.g. Fashion, Poetry, Politics + Site topic + Tap <b>%1$s</b> to continue. Skip for today + View more prompts + %d answers Share blogging prompt ✓ Answered - %d answers Answer prompt Prompts All This colour combination may be hard for people to read. Try using a brighter background colour and/or a darker text colour. - Failed to insert media.\nTap for more info. This colour combination may be hard for people to read. Try using a darker background colour and/or a brighter text colour. - What’s your website about? + Failed to insert media.\nTap for more info. Choose a topic from the list below or type your own. - Adding category - Home + What’s your website about? Weekly Roundup - There was a problem communicating with the site. An HTTP error code 401 was returned. + Home + Adding category Which email app do you use? - Unable to read the WordPress site at that URL. Tap on help icon to view the FAQ. + There was a problem communicating with the site. An HTTP error code 401 was returned. XML-RPC calls seem blocked on this site (error code 401). If attempt to log in fails, tap on help icon to view the FAQ. + Unable to read the WordPress site at that URL. Tap on help icon to view the FAQ. XML-RPC services are disabled on this site. Menu Your search includes characters not supported in WordPress.com domains. The following characters are allowed: A–Z, a–z, 0–9. - An error occurred while updating the notification content - Today\'s Stats Check your internet connection and refresh the page. + Today\'s Stats + An error occurred while updating the notification content Edit - Mark as spam - Move to bin Failed to moderate comments + Move to bin + Mark as spam Unapprove - Navigates to layout selection screen Tiled gallery settings + Navigates to layout selection screen Gallery style You can connect your %s account on the WordPress.com website. When you\'re done, return to the app to change your Social settings. - WordPress - Automattic logo - Back icon App icon - Work from anywhere - Terms of service - Privacy policy - Source code - Day One - Jetpack - Pocket Casts - Simplenote - Tumblr + Back icon + Automattic logo + WordPress WooCommerce - You can edit this block using the web version of the editor. - Share with friends - Rate us - Instagram - Twitter - Legal and more - Automattic family - Work with us - Note: you must allow WordPress.com login to edit this block in the mobile editor. - Open Jetpack security settings - We\'re having trouble loading your site\'s data at the moment. - ADD MEDIA - Address settings + Tumblr + Simplenote + Pocket Casts + Jetpack + Day One + Source code + Privacy policy + Terms of service + Work from anywhere + Work with us + Automattic family + Legal and more + Twitter + Instagram + Rate us + Share with friends + You can edit this block using the web version of the editor. + Open Jetpack security settings + Note: you must allow WordPress.com login to edit this block in the mobile editor. Note: layout may vary between themes and screen sizes - Video not uploaded! Uploading videos longer than five minutes requires a paid plan. - Couldn\'t update dashboard. - The dashboard is not updated. Please check your connection and then pull to refresh. + Address settings + ADD MEDIA + We\'re having trouble loading your site\'s data at the moment. Some data hasn\'t loaded - California privacy notice + The dashboard is not updated. Please check your connection and then pull to refresh. + Couldn\'t update dashboard. + Video not uploaded! Uploading videos longer than five minutes requires a paid plan. Acknowledgements - Double tap to select font size - Font size - Get support - More support options - Selected: default - The basics - Blog - About %1$s - Legal and more - Acknowledgements + California privacy notice Version %1$s - There was an error getting post data - View all comments - Be the first to comment - Follow conversation - %1$s (%2$s) - Contact support + Acknowledgements + Legal and more + About %1$s + Blog + The basics + Selected: default + More support options + Get support + Font size + Double tap to select font size Double tap to select default font size - Follow conversation settings + Contact support + %1$s (%2$s) + Follow conversation + Be the first to comment + View all comments + There was an error getting post data There was an error getting comments - About WordPress - Copy URL from the clipboard, %s - Featured image + Follow conversation settings From clipboard - Copy link - Author - Link copied to clipboard - Switched to HTML mode - Switched to visual mode - Create your next post - Posting regularly helps build your audience! + Featured image + Copy URL from the clipboard, %s + About WordPress Create a post + Posting regularly helps build your audience! + Create your next post + Switched to visual mode + Switched to HTML mode + Link copied to clipboard + Author + Copy link Adding a custom domain makes it easy for visitors to find your site - Upcoming scheduled posts - Untitled - Create your first post - Posts appear on your blog page in reverse chronological order. It\'s time to share your ideas with the world! Add your domain + Posts appear on your blog page in reverse chronological order. It\'s time to share your ideas with the world! + Create your first post + Untitled + Upcoming scheduled posts Work on a draft post <span style=\"color:#008000;\">Free for the first year </span><span style=\"color:#50575e;\"><s>%s /year</s></span> Your domains will redirect to your site at %s Create link - You\'re following this conversation. You will receive notifications by email when new comments are published. - Enable in-app notifications - Unfollow conversation - Mark as sticky - Stick post to the front page - Sticky - Domains Select domain - Could not enable in-app notifications - Could not disable in-app notifications + Domains + Sticky + Stick post to the front page + Mark as sticky + Unfollow conversation + Enable in-app notifications + You\'re following this conversation. You will receive notifications by email when new comments are published. Manage follow conversation options, popup window - Following this conversation\nEnable in-app notifications? - Unsubscribed from this conversation - In-app notifications enabled + Could not disable in-app notifications + Could not enable in-app notifications In-app notifications disabled - You have a free one-year domain registration included with your plan + In-app notifications enabled + Unsubscribed from this conversation + Following this conversation\nEnable in-app notifications? Search for a domain Domains purchased on this site will redirect visitors to <b>%s</b> - Add a domain - Manage domains + You have a free one-year domain registration included with your plan Claim your free domain + Manage domains + Add a domain <span style=\"color:#d63638;\">Expires on %s</span> - Change site address - Primary site address - Your site domains Expires on %s + Your site domains + Primary site address + Change site address Your free WordPress.com address is - %s<span style=\"color:#50575e;\"> /year</span> <span style=\"color:#B26200;\">%1$s for the first year </span><span style=\"color:#50575e;\"><s>%2$s /year</s></span> - Done - Name - Comment - Web address - Email address - User name cannot be empty - Web address not valid - User email not valid - Comment cannot be empty - There are unsaved changes + %s<span style=\"color:#50575e;\"> /year</span> Do you want to discard them? + There are unsaved changes + Comment cannot be empty + User email not valid + Web address not valid + User name cannot be empty + Email address + Web address + Comment + Name + Done Embed block previews are coming soon Weekly Roundup - Double tap to view embed options. Embed options + Double tap to view embed options. Site created! Complete another task. - <a href=\"\">You and one blogger</a> like this. - <a href=\"\">You and %1$s bloggers</a> like this. - <a href=\"\">one blogger</a> likes this. <a href=\"\">%1$s bloggers</a> like this. + <a href=\"\">one blogger</a> likes this. + <a href=\"\">You and %1$s bloggers</a> like this. + <a href=\"\">You and one blogger</a> like this. <a href=\"\">You</a> like this. - Unknown error fetching recommended app template - Get your domain Line Height + Get your domain %s - Domains - Quick Links - Share WordPress with a friend - No response received + Unknown error fetching recommended app template Invalid response received + No response received Automattic Apps – Apps for any screen - You\'ll get reminders to blog <b>every day</b> at <b>%s</b>. - Notification time + Share WordPress with a friend + Quick Links + Domains Weekly Roundup: %s - Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block + Notification time + You\'ll get reminders to blog <b>every day</b> at <b>%s</b>. %1$s a week at %2$s - How to edit your page - How to edit your post - Move blocks - Navigates to select %s - Select a colour above + Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block Selected: %s - Changes to featured image will not be affected by the undo/redo buttons. + Select a colour above + Navigates to select %s + Move blocks + How to edit your post + How to edit your page Customise blocks + Changes to featured image will not be affected by the undo/redo buttons. Applies the setting You can rearrange blocks by tapping a block and then tapping the up and down arrows that appear on the bottom left side of the block to move it above or below other blocks. - To remove a block, select the block and click the three dots in the bottom right of the block to view the settings. From there, choose the option to remove the block. Welcome to the world of blocks - %s block, newly available + To remove a block, select the block and click the three dots in the bottom right of the block to view the settings. From there, choose the option to remove the block. Some blocks have additional settings. Tap the settings icon on the bottom right of the block to view more options. - Once you become familiar with the names of different blocks, you can add a block by typing a forward slash followed by the block name – for example, /image or /heading. + %s block, newly available Rich text editing + Once you become familiar with the names of different blocks, you can add a block by typing a forward slash followed by the block name – for example, /image or /heading. Make your content stand out by adding images, gifs, videos, and embedded media to your pages. - Embed media Give it a try by adding a few blocks to your post or page! + Embed media Each block has its own settings. To find them, tap on a block. Its settings will appear on the toolbar at the bottom of the screen. - Blocks are pieces of content that you can insert, rearrange, and style without needing to know how to code. Blocks are an easy and modern way for you to create beautiful layouts. Build layouts + Blocks are pieces of content that you can insert, rearrange, and style without needing to know how to code. Blocks are an easy and modern way for you to create beautiful layouts. Blocks allow you to focus on writing your content, knowing that all the formatting tools you need are there to help you get your message across. Arrange your content into columns, add Call to Action buttons, and overlay images with text. - %1$s out of %2$s complete Add a new block at any time by tapping on the + icon in the toolbar on the bottom left. - Failed to moderate one or more comments + %1$s out of %2$s complete Learn the basics with a quick walkthrough. + Failed to moderate one or more comments Create site Get your site up and running in just a few quick steps Create your WordPress website @@ -907,24 +939,24 @@ Language: en_GB Show me around Want a little help managing this site with the app? Create a new site + You can switch sites at any time. Choose a site to open We\'re sorry, Jetpack Scan is not compatible with multisite WordPress installations at this time. - You can switch sites at any time. WordPress multisites are not supported Invalid URL. Please enter a valid URL. - Jetpack Backup for Multisite installations provides downloadable backups, no one-click restores. For more information %1$s. - visit our documentation page - Embed caption. Empty Embed caption. %s + Embed caption. Empty + visit our documentation page + Jetpack Backup for Multisite installations provides downloadable backups, no one-click restores. For more information %1$s. Posting regularly can help keep your readers engaged, and attract new visitors to your site. Tip You can update this anytime - You can update this anytime via My Site > Settings > Blogging reminders. Select the days on which you want to blog + You can update this anytime via My Site > Settings > Blogging reminders. You have no reminders set. + You\'ll get reminders to blog %1$s a week on %2$s at %3$s. Reminders removed! All set! - You\'ll get reminders to blog %1$s a week on %2$s at %3$s. Update None set %s a week @@ -937,66 +969,66 @@ Language: en_GB Editing reusable blocks is not yet supported on WordPress for iOS Editing reusable blocks is not yet supported on WordPress for Android Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”. - Notify me Done + Notify me <a href=\"%1$s\">Enter your server credentials</a> to enable one-click site restores from backups. - WordPress for Android support - Create category - Remove as Featured Image Set as Featured Image + Remove as Featured Image + Create category + WordPress for Android support Manage your site\'s categories - The content of your latest posts page is automatically generated and cannot be edited. Categories Reminders + The content of your latest posts page is automatically generated and cannot be edited. 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 Don\'t show again + View storage + 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. Insufficient device storage Y-axis position X-axis position - %s has no URL set - %s has URL set - Slash inserter results Type a URL - %s block + Slash inserter results + %s has URL set + %s has no URL set %s converted to normal blocks - Invalid URL. Audio file not found. - Media options + %s block Opacity - Double tap to open action sheet to add image or video - Double tap to open bottom sheet to add image or video - Drag to adjust focal point + Media options + Invalid URL. Audio file not found. Insert crosspost - Columns settings - Crosspost + Drag to adjust focal point + Double tap to open bottom sheet to add image or video + Double tap to open action sheet to add image or video Current unit is %s + Crosspost %s converted to regular block - Add link text + Columns settings Add link to %s + Add link text Add image or video - Could not find media file in path The specified path is a directory instead of a media file - <a href=\"%1$s\">Enter your server credentials</a> to fix threat. - <a href=\"%1$s\">Enter your server credentials</a> to fix threats. - Media was empty - This file type is not allowed + Could not find media file in path Unexpected empty file path for media + This file type is not allowed + Media was empty + <a href=\"%1$s\">Enter your server credentials</a> to fix threats. + <a href=\"%1$s\">Enter your server credentials</a> to fix threat. Double tap to add a link. Try with another account See instructions If you already have a site, you\'ll need to install the free Jetpack plugin and connect it to your WordPress.com account. Your profile photo To use this app for %1$s you\'ll need to have the Jetpack plugin installed and connected to your WordPress.com account. - Move image backwards Move image forwards + Move image backwards Width settings - Column settings Link Rel + Column settings No description - User profile bottom sheet information - Site (Untitled) + Site + User profile bottom sheet information Likes list %s Two Three @@ -1004,564 +1036,564 @@ Language: en_GB %s social icon Mention NEW - Preview page Preview post + Preview page Retry GIF One - No preview available Add title + No preview available Loading - %s link Text colour + %s link Padding - Featured Four + Featured Add image - Create embed Custom URL + Create embed Column %d More Briefly describe the link to help screen reader user Add blocks No Jetpack sites found - Transform block… - Transform %s to What is alt text? + Transform %s to + Transform block… Failed to insert media. - %d Likes - Error loading like data. %s. - %1$s transformed to %2$s Failed to insert audio file. Describe the purpose of the image. Leave empty if the image is purely decorative. - Suggestion: + %1$s transformed to %2$s + Error loading like data. %s. + %d Likes One Like - Search block label. Current text is - Search blocks - Search button. Current button text is - Search input field. + Suggestion: Use icon button - Double tap to edit button text - Double tap to edit label text - Double tap to edit placeholder text - Hide search heading - Inside - No custom placeholder set + Search input field. + Search button. Current button text is + Search blocks + Search block label. Current text is Outside + No custom placeholder set + Inside + Hide search heading + Double tap to edit placeholder text + Double tap to edit label text + Double tap to edit button text double tap to change unit - Unreplied - No unreplied comments - No network available. - An unknown error occurred while getting likes. - An error occurred while getting likes data - %1$s. %2$s is %3$s %4$s. - Button position - Cancel search - Clear search Current placeholder text is - Search settings + Clear search + Cancel search + Button position + %1$s. %2$s is %3$s %4$s. + An error occurred while getting likes data + An unknown error occurred while getting likes. + No network available. + No unreplied comments + Unreplied ADD LINK - Disallowed comments + Search settings Always allowed IP addresses + Disallowed comments Add button text Follow topics - Download - Dismiss A new way to create and publish engaging content on your site. + Dismiss + Download Threats were successfully fixed. Please confirm you want to fix all %s active threats. The scan found %1$s potential threats with %2$s. Please review them below and take action, or tap the fix all button. We are %3$s if you need us. We\'re hard at work in the background fixing these threats. In the meantime, feel free to continue to use your site as normal, you can check the progress at any time. Edit focal point - <b>All tasks completed</b><br/>You’ll reach more people. Nice job! - Type a name for your site - example.com - Double tap to open Action Sheet to edit, replace, or clear the image Double tap to open Bottom Sheet to edit, replace, or clear the image + Double tap to open Action Sheet to edit, replace, or clear the image + example.com + Type a name for your site + <b>All tasks completed</b><br/>You’ll reach more people. Nice job! <b>All tasks completed</b><br/>You’ve customised your site. Well done! - Once this invite link is disabled, nobody will be able to use it to join your team. Are you sure? Didn\'t mean to create a new account? Go back to re-enter your email address. - Use this link to onboard your team members without having to invite them one by one. Anybody visiting this URL will be able to sign up to your organisation, even if they received the link from somebody else, so make sure that you share it with trusted people. - Unknown error fetching invite links data - There was an error getting roles - There was an error getting data for role %1$s - No response received - Invalid response received + Once this invite link is disabled, nobody will be able to use it to join your team. Are you sure? Disable invite link - Invite Link - Refresh links status - Generate new link - Share invite link - Disable invite link + Invalid response received + No response received + There was an error getting data for role %1$s + There was an error getting roles + Unknown error fetching invite links data + Use this link to onboard your team members without having to invite them one by one. Anybody visiting this URL will be able to sign up to your organisation, even if they received the link from somebody else, so make sure that you share it with trusted people. Expires %1$s - Threats found + Disable invite link + Share invite link + Generate new link + Refresh links status + Invite Link Threat found - <b>Scan Finished</b> <br> No threats found - <b>Scan Finished</b> <br> One potential threat found + Threats found <b>Scan Finished</b> <br> %s potential threats found - Disable + <b>Scan Finished</b> <br> One potential threat found + <b>Scan Finished</b> <br> No threats found Fixing Threat + Disable Check your pages and make changes, or add or remove pages. - Give your site a name that reflects its personality and topic. - Automatically share new posts to your social media. + View your site Discover and follow sites that inspire you. Social sharing - View your site + Automatically share new posts to your social media. + Give your site a name that reflects its personality and topic. Check your site stats - We couldn\'t find the status to say how long your downloadable backup will take. We\'ll still attempt to create your downloadable backup file. - We’ll notify you when its done. - Clock icon - Checkmark icon + We couldn\'t find the status to say how long your downloadable backup will take. Hmm, we couldn\'t find your downloadable backup status - We couldn\'t restore your site - Hmm, we couldn\'t find your restore status - We couldn\'t find the status to say how long your restore will take. + Checkmark icon + Clock icon + We’ll notify you when its done. We\'ll still attempt to restore your site. - (excludes themes, plugins, and uploads) - (SQL) - We couldn\'t create your backup - Are you sure you want to revert your site back to %1$s at %2$s?\n Anything you changed since then will be lost. + We couldn\'t find the status to say how long your restore will take. + Hmm, we couldn\'t find your restore status + We couldn\'t restore your site Confirm - Uploading… - Items included in this download - WordPress root + Are you sure you want to revert your site back to %1$s at %2$s?\n Anything you changed since then will be lost. + We couldn\'t create your backup + (SQL) + (excludes themes, plugins, and uploads) WP-content directory - Double tap to select an audio file - Failed to insert audio file. Please tap for options. - Lock icon - No application can handle this request. - OPEN - Problem opening the audio - Replace audio + WordPress root + Items included in this download + Uploading… Replace file - Optional: enter a custom message to be sent with your invitation. - Choose audio from device - Use this audio - Log in or sign up with WordPress.com - Audio caption. Empty - Audio caption. %s - Audio Player - Choose audio + Replace audio + Problem opening the audio + OPEN + No application can handle this request. + Lock icon + Failed to insert audio file. Please tap for options. + Double tap to select an audio file Double tap to listen to the audio file + Choose audio + Audio Player audio file + Audio caption. %s + Audio caption. Empty Add audio - here to help - Found - Fixed + Log in or sign up with WordPress.com + Use this audio + Choose audio from device + Optional: enter a custom message to be sent with your invitation. Learn more about roles + Fixed + Found + here to help The scan found one potential threat with %1$s. Please review the threat below and take action, or tap the fix all button. We are %2$s if you need us. - Welcome to Jetpack Scan! We\'re scoping out your site, setting up to do a full scan. We\'ll let you know if we spot any issues that might impact a scan, then your first full scan will start. To review your site again, run a manual scan, or wait for Jetpack to scan your site later today. + Welcome to Jetpack Scan! We\'re scoping out your site, setting up to do a full scan. We\'ll let you know if we spot any issues that might impact a scan, then your first full scan will start. Welcome to Jetpack Scan, we are taking a first look at your site now and the results will be with you soon. We\'re hard at work in the background fixing this threat. In the meantime, feel free to continue to use your site as normal, you can check the progress at any time. We will send you a notification if a threat is found. In the meantime, feel free to continue to use your site as normal, you can check the progress at anytime. - Jetpack Scan couldn\'t complete a scan of your site. Please check to see if your site is down – if it\'s not, try again. If it is, or if Jetpack Scan is still having problems, contact our support team. Fixing Threats - Your site has been successfully backed up - Creating downloadable backup - Backing up site from %1$s %2$s - Backing up site + Jetpack Scan couldn\'t complete a scan of your site. Please check to see if your site is down – if it\'s not, try again. If it is, or if Jetpack Scan is still having problems, contact our support team. Something went wrong - Select audio - Your site is being backed up\nBacking up from %1$s %2$s + Backing up site + Backing up site from %1$s %2$s + Creating downloadable backup + Your site has been successfully backed up Your site has been successfully backed up\nBacked up from %1$s %2$s - Done button - Error icon + Your site is being backed up\nBacking up from %1$s %2$s + Select audio There is another restore running. - No need to wait around. We\'ll notify you when your site has been restored. - Your site has been restored - All of your selected items are now restored back to %1$s %2$s. - Visit site - Restore icon - Done button - Visit site button + Error icon + Done button Restore failed - Confirm restore site button - Currently restoring site - We\'re restoring your site back to %1$s %2$s. + Visit site button + Done button + Restore icon + Visit site + All of your selected items are now restored back to %1$s %2$s. + Your site has been restored + No need to wait around. We\'ll notify you when your site has been restored. Restore site icon - Restore site button - Warning + We\'re restoring your site back to %1$s %2$s. + Currently restoring site + Confirm restore site button Red circle image with exclamation point - Done - Done button - Cloud with X icon - Restore - Choose the items to restore: - Restore Site - %1$s %2$s is the selected point for your restore. - Restore site + Warning + Restore site button Restore icon - Select %1$s Homepage %2$s to edit your Homepage. - Review site pages - Change, add, or remove your site\'s pages. - Select %1$s Pages %2$s to see your page list. - Mobile - Tablet - Download failed - Marked post as seen - Marked post as unseen - Can\'t toggle seen status of this post - Not enough available site storage - Media upload failed.\n%1$s - Mark as seen + Restore site + %1$s %2$s is the selected point for your restore. + Restore Site + Choose the items to restore: + Restore + Cloud with X icon + Done button + Done + Download failed + Tablet + Mobile + Select %1$s Pages %2$s to see your page list. + Change, add, or remove your site\'s pages. + Review site pages + Select %1$s Homepage %2$s to edit your Homepage. Mark as unseen - Error fixing threats. Please contact our support. + Mark as seen + Media upload failed.\n%1$s + Not enough available site storage + Can\'t toggle seen status of this post + Marked post as unseen + Marked post as seen Error getting fix status. Please contact our support. Threat was successfully fixed. + Error fixing threats. Please contact our support. Please confirm you want to fix one active threat. - Threat ignored. - Error ignoring threat. Please contact support. Fix all threats + Error ignoring threat. Please contact support. + Threat ignored. You shouldn’t ignore a security issue unless you are absolute sure it’s harmless. If you choose to ignore this threat, it will remain on your site <b>%s</b>. Error fixing threat. Please contact our support. - Your first backup will appear here within 24 hours and you will receive a notification once the backup has been completed - No matching backups found - Try adjusting your date range - Scan History - History - Preparing to scan - Scanning files - All - Fixed - No items found - Ignored - Fixing threat Threat ignored Threat fixed on %s - There was a problem handling the request. Please try again later. + Fixing threat + Ignored + No items found + Fixed + All + Scanning files + Preparing to scan + History + Scan History + Try adjusting your date range + No matching backups found + Your first backup will appear here within 24 hours and you will receive a notification once the backup has been completed Your first backup will be ready soon + There was a problem handling the request. Please try again later. Move to bottom Change block position - Share link button - We\'ve also emailed you a link to your file. - icon Upload - Your backup is now available for download - We successfully created a backup of your site from %1$s %2$s. - Download - Share link - Downloadable backup ready icon + icon + We\'ve also emailed you a link to your file. + Share link button Download button - Currently creating a downloadable backup of your site - We\'re creating a downloadable backup of your site from %1$s %2$s. - Creating downloadable backup icon + Downloadable backup ready icon + Share link + Download + We successfully created a backup of your site from %1$s %2$s. + Your backup is now available for download Your Backup No need to wait around. We\'ll notify you when your backup is ready. - %1$s %2$s is the selected point to create a downloadable backup. - Create downloadable backup button - There was a problem handling the request. Please try again later. - There is another download running. + Creating downloadable backup icon + We\'re creating a downloadable backup of your site from %1$s %2$s. + Currently creating a downloadable backup of your site Download Backup + There is another download running. + There was a problem handling the request. Please try again later. + Create downloadable backup button + %1$s %2$s is the selected point to create a downloadable backup. %1$s · %2$s · - %1$s · %1$s · %2$s - Jetpack Scan will delete the affected file or directory. - Jetpack Scan will update to a newer version (%s). - Jetpack Scan will edit the affected file or directory. - Jetpack Scan will resolve the threat. - Fix threat - Ignore threat - Get a free estimate - user + %1$s · crosspost - Please type to filter the list of suggestions. - No %s suggestions available. - There was a problem loading suggestions. + user No matching %s. + There was a problem loading suggestions. + No %s suggestions available. + Please type to filter the list of suggestions. + Get a free estimate + Ignore threat + Fix threat + Jetpack Scan will resolve the threat. + Jetpack Scan will edit the affected file or directory. + Jetpack Scan will update to a newer version (%s). + Jetpack Scan will delete the affected file or directory. 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 - What was the problem? - The technical details - Threat found in file: - How will we fix it? - How did Jetpack fix it? Resolving the threat - Database %s threats - %s: malicious code pattern - Miscellaneous vulnerability - Vulnerability found in WordPress - Threat found %s - Vulnerability found in plugin - Vulnerability found in theme + How did Jetpack fix it? + How will we fix it? + Threat found in file: + The technical details + What was the problem? Threat Details - Vulnerable Plugin: %1$s (version %2$s) + Vulnerability found in theme + Vulnerability found in plugin + Threat found %s + Vulnerability found in WordPress + Miscellaneous vulnerability Vulnerable Theme: %1$s (version %2$s) - this site - Threat found + Vulnerable Plugin: %1$s (version %2$s) + %s: malicious code pattern + Database %s threats %s: infected core file + Threat found Fix All - %s hour(s) ago - %s minute(s) ago a few seconds ago + %s minute(s) ago + %s hour(s) ago + this site The last Jetpack scan ran %1$s and did not find any risks. %2$s - Activity Type filter (%s types selected) - Backup - Scan state icon - Scan now - Scan again - Don\'t worry about a thing Your site may be at risk - Please check your internet connection and retry. - No activities available - No activities recorded in the selected date range. - Activity Type filter + Don\'t worry about a thing + Scan again + Scan now + Scan state icon + Backup + Activity Type filter (%s types selected) %1$s (showing %2$s items) - Date Range filter - Activity Type (%s) + Activity Type filter + No activities recorded in the selected date range. + No activities available + Please check your internet connection and retry. No connection - No matching events found + Activity Type (%s) + Date Range filter Try adjusting your date range or activity type filters - Create downloadable backup icon - WordPress Themes - WordPress Plugins - Media Uploads - (includes wp-config.php and any non-WordPress files) + No matching events found Site database - Restore to this point - Download backup - Choose file - Error - Backup Download - Download Backup - Create downloadable backup + (includes wp-config.php and any non-WordPress files) + Media Uploads + WordPress Plugins + WordPress Themes + Create downloadable backup icon Create downloadable file - Date Range + Create downloadable backup + Download Backup + Backup Download + Error + Choose file + Download backup + Restore to this point Activity Type - Duplicate - Post sync conflict - The post you are trying to copy has two versions that are in conflict, or you recently made changes but didn\'t save them.\nEdit the post first to resolve any conflict or proceed with copying the version from this app. - Edit the post first - Copy the version from this app + Date Range Filter by Activity Type + Copy the version from this app + Edit the post first + The post you are trying to copy has two versions that are in conflict, or you recently made changes but didn\'t save them.\nEdit the post first to resolve any conflict or proceed with copying the version from this app. + Post sync conflict + Duplicate Story being saved, please wait… - Copy file URL - Edit file - Failed to save files.\nPlease tap for options. - Failed to upload files.\nPlease tap for options. - File block settings File name - Error fetching subscription status for post - Could not subscribe to comments for this post - Could not unsubscribe from comments for this post - Jetpack + File block settings + Failed to upload files.\nPlease tap for options. + Failed to save files.\nPlease tap for options. + Edit file + Copy file URL Choose a domain - Follow conversation by email + Jetpack Following conversation by email - Apply - Clear - No response received + Follow conversation by email + Could not unsubscribe from comments for this post + Could not subscribe to comments for this post + Error fetching subscription status for post Invalid response received + No response received + Clear + Apply One or more slides have not been added to your Story because Stories don\'t support GIF files at the moment. Please choose a static image or video background instead. - This story was edited on a different device and the ability to edit certain objects may be limited. - Can\'t edit Story - Unable to load media for this story. Check your internet connection and try again in a moment. - Can\'t edit Story - We couldn\'t find the media for this story on the site. GIF files not supported + We couldn\'t find the media for this story on the site. + Can\'t edit Story + Unable to load media for this story. Check your internet connection and try again in a moment. + Can\'t edit Story + This story was edited on a different device and the ability to edit certain objects may be limited. Limited Story Editing Media has been removed. Try re-creating your Story. - Layouts not available while offline - Tap retry when you\'re back online. - Please check your internet connection and retry. - Delete - Next - Done - Discard changes? - Any changes made will not be saved. - Discard - Text Background + Text + Discard + Any changes made will not be saved. + Discard changes? + Done + Next + Delete There was an error while selecting the theme. - Scan - Welcome! - No recent posts - Try following more topics to broaden the search - Follow topics - Find your connected email + Please check your internet connection and retry. + Tap retry when you\'re back online. + Layouts not available while offline Continue with store credentials - <b>Madison Ruiz</b> liked your post - You received <b>50 likes</b> on your site today + Find your connected email + Follow topics + Try following more topics to broaden the search + No recent posts + Welcome! + Scan <b>Johan Brandt</b> responded to your post - Choose - Scrollable block menu closed. + You received <b>50 likes</b> on your site today + <b>Madison Ruiz</b> liked your post Scrollable block menu opened. Select a block. - Categories - Not set - Categories - Add New Category - Add Category - Layouts not available due to an error - Tap retry or create a blank page using the button below. - Layouts not available while offline + Scrollable block menu closed. + Choose Tap retry when you\'re back online, or create a blank page using the button below. - I am so inspired by photographer Cameron Karsten’s work. I will be trying these techniques on my next - Pamela Nguyen - Web News - Rock ‘n’ Roll Weekly - Art - Cooking - Football - Gardening - Music - Politics - My Top Ten Cafés - The World\'s Best Fans + Layouts not available while offline + Tap retry or create a blank page using the button below. + Layouts not available due to an error + Add Category + Add New Category + Categories + Not set + Categories Museums in London - Welcome to the world’s most popular website builder. - With the powerful editor, you can post on the go. - Watch your audience grow with in-depth analytics. + The World\'s Best Fans + My Top Ten Cafés + Politics + Music + Gardening + Football + Cooking + Art + Rock ‘n’ Roll Weekly + Web News + Pamela Nguyen + I am so inspired by photographer Cameron Karsten’s work. I will be trying these techniques on my next Getting Inspired - See comments and notifications in real time. Follow your favourite sites and discover new blogs. - Sites to follow + Watch your audience grow with in-depth analytics. + See comments and notifications in real time. + With the powerful editor, you can post on the go. + Welcome to the world’s most popular website builder. Media loading failed + Sites to follow We are working hard to add more blocks with each release. \'%s\' is not fully supported - They’re published as a new blog post on your site, so your audience never misses out on a thing. - Create Story Post - Choose images - Edit using web editor Help button - Combine photos, videos, and text to create engaging and tappable story posts that your visitors will love. + Edit using web editor + Choose images + Create Story Post + They’re published as a new blog post on your site, so your audience never misses out on a thing. Story posts don\'t disappear - Page created - Blank page created - Introducing Story Posts - How to create a story post - Example story title + Combine photos, videos, and text to create engaging and tappable story posts that your visitors will love. Now stories are for everyone - Choose from WordPress Media Library - Media insert failed: %s + Example story title + How to create a story post + Introducing Story Posts + Blank page created + Page created Media insert failed. + Media insert failed: %s + Choose from WordPress Media Library Back - By - Follow topics to discover new blogs Get Started - Uploading media - Uploading stock media - Uploading gif media - Open Website - Mark as Spam - Unmark as Spam + Follow topics to discover new blogs + By This referrer can\'t be marked as spam + Unmark as Spam + Mark as Spam + Open Website + Uploading gif media + Uploading stock media + Uploading media Search or type URL Add this telephone link - Add this email link Add this link + Add this email link No internet connection.\nSuggestions are unavailable. - %s selected - %s - You need to grant the app audio recording permission in order to record video - Casual - Classic - Strong - Playful - Modern Bold - Browse for items - Unable to show this comment - Microphone - Hmm, we can\'t find a WordPress.com account connected to this email address. + Modern + Playful + Strong + Classic + Casual + You need to grant the app audio recording permission in order to record video + %s + %s selected Get a login link by email + Hmm, we can\'t find a WordPress.com account connected to this email address. + Microphone + Unable to show this comment + Browse for items Report this post - %1$s more items - Your action is not allowed - Internal server error occurred Welcome to Reader. Discover millions of blogs at your fingertips. + Internal server error occurred + Your action is not allowed + %1$s more items Select a layout Note: column layout may vary between themes and screen sizes - Create a post - Create a page Create a post or story - Hide - You might like - Paste block after - Updates the title. - Video caption. Empty - View Storage - Couldn\'t find Story slide - Operation in progress, try again - Error saving image - Video could not be saved - This device doesn\'t support Camera2 API. - An error occurred while playing your video - Page title. Empty + Create a page + Create a post + You might like + Hide + Video caption. Empty + Updates the title. + Paste block after Page title. %s - One slide requires action - %1$d slides require action - Manage - Unable to save one slide - Unable to save %1$d slides - Retry saving, or delete the slides, then try publishing your story again. - Insufficient device storage + Page title. Empty + An error occurred while playing your video + This device doesn\'t support Camera2 API. + Video could not be saved + Error saving image + Operation in progress, try again + Couldn\'t find Story slide + View Storage We need to save the story on your device before it can be published. Review your storage settings and remove files to free up space. - \"%1$s\" published - Unable to upload \"%1$s\" + Insufficient device storage + Retry saving, or delete the slides, then try publishing your story again. + Unable to save %1$d slides + Unable to save one slide + Manage + %1$d slides require action + One slide requires action Unable to upload \"%1$s\" + Unable to upload \"%1$s\" + \"%1$s\" published Uploading \"%1$s\"… - Saving \"%1$s\"… - several stories - One slide remaining %1$d slides remaining - errored - Change text alignment - Change text colour - Delete story slide? - This slide will be removed from your story. - This slide has not been saved yet. If you delete this slide, you will lose any edits you have made. - Delete - Discard story post? - Your story post will not be saved as a draft. - Discard + One slide remaining + several stories + Saving \"%1$s\"… Untitled - unselected + Discard + Your story post will not be saved as a draft. + Discard story post? + Delete + This slide has not been saved yet. If you delete this slide, you will lose any edits you have made. + This slide will be removed from your story. + Delete story slide? + Change text colour + Change text alignment + errored selected - Tap %1$s Create. %2$s Then select <b>Blog post</b> - Create a post, page, or story - Stickers - Text - Sound - Flip - Flash - Saving - Saved - Retry + unselected + Slide + Retry + Saved + Close + Share to + SHARE Saved to photos - Create a post or story - Give your story a title - Get started by choosing from a wide variety of pre-made page layouts. Or just start with a blank page. - Create blank page - Create page - Preview - Capture - Flip camera + Retry + Saved + Saving + Flash + Flip + Sound + Text + Stickers Flash - SHARE - Share to - Close - Saved - Retry - Slide + Flip camera + Capture + Preview + Create page + Create blank page + Get started by choosing from a wide variety of pre-made page layouts. Or just start with a blank page. Choose a layout - Storage quota exceeded - Cannot upload file.\nStorage quota was exceeded. - Unable to find the linked page jump - Editing site icons on self-hosted WordPress sites requires the Jetpack plugin. - Story post + Give your story a title + Create a post or story + Create a post, page, or story + Tap %1$s Create. %2$s Then select <b>Blog post</b> Choose from device + Story post + Editing site icons on self-hosted WordPress sites requires the Jetpack plugin. + Unable to find the linked page jump + Cannot upload file.\nStorage quota was exceeded. + Storage quota exceeded Add file Replace video Replace image or video + Convert to link Choose video Choose image or video Choose image Block removed - If you continue with Google and don\'t already have a WordPress.com account, you are creating an account and you agree to our %1$sTerms of Service%2$s. - Signup confirmation Enter your existing site address - Convert to link + Signup confirmation + If you continue with Google and don\'t already have a WordPress.com account, you are creating an account and you agree to our %1$sTerms of Service%2$s. By continuing, you agree to our %1$sTerms of Service%2$s. We’ll use this email address to create your new WordPress.com account. We’ve emailed you a signup link to create your new WordPress.com account. Check your email on this device, and tap the link in the email you receive from WordPress.com. @@ -1573,31 +1605,31 @@ Language: en_GB Not seeing the email? Check your Spam or Junk Mail folder. Check your email on this device and tap the link in the email you received from WordPress.com. We\'ll email you a link that\'ll log you in instantly, no password needed. - Reset your password Check email Get Started Enter your email address to log in or create a WordPress.com account. Or type your password Create account Send link by email + Reset your password There was a problem handling the request. Please try again later. - Tap <b>%1$s</b> to set a new title Check your site title + Tap <b>%1$s</b> to set a new title Binning this post will also discard local changes, are you sure you want to continue? %s block options - The Site Title can only be changed by a user with the administrator role. - Block copied - Block cut - Block duplicated - Block pasted - Copied block - Copy block - Duplicate block Remove block - Unsaved changes - Couldn\'t update site title. Check your network connection and try again. - Topic + Duplicate block + Copy block + Copied block + Block pasted + Block duplicated + Block cut + Block copied + The Site Title can only be changed by a user with the administrator role. The Site Title is displayed in the title bar of a web browser and is displayed in the header for most themes. + Topic + Couldn\'t update site title. Check your network connection and try again. + Unsaved changes Open link in a browser Navigates to the previous content sheet Navigates to customise the gradient @@ -1611,68 +1643,69 @@ Language: en_GB Content structure Everyone Me - Not set Dismiss - Tags - Publishing to - Schedule Now - Submit Now - Save Now - Back - Add Tags + Not set Tags help tell readers what a post is about. - Binned posts can\'t be edited. Do you want to change the status of this post to \"draft\", so you can work on it? - Move to Draft - Cancel - Publish Date Publish Date - The California Consumer Privacy Act (\"CCPA\") requires us to provide California residents with some additional information about the categories of personal information we collect and share, where we get that personal information, and how and why we use it. - Read CCPA privacy notice - Publish Date - Scheduled - Binned - Published - Select a few to continue - Done + Add Tags + Back + Save Now + Submit Now + Schedule Now + Publishing to + Tags + Publish Date + Cancel + Move to Draft + Binned posts can\'t be edited. Do you want to change the status of this post to \"draft\", so you can work on it? Move post to Drafts? - Choose your topics Choose your topics - Update Now - Status and Visibility + Choose your topics + Done + Select a few to continue + Published + Binned + Scheduled + Publish Date + Read CCPA privacy notice + The California Consumer Privacy Act (\"CCPA\") requires us to provide California residents with some additional information about the categories of personal information we collect and share, where we get that personal information, and how and why we use it. Privacy notice for California users + Status and Visibility + Update Now %1$s · Open Block Actions Menu Move to top - Double tap to open Action Sheet with available options - Double tap to open Bottom Sheet with available options Insert mention - Classic Blog - Static Homepage - Posts Page - Select Page - Set as Homepage - Set as Posts Page + Double tap to open Bottom Sheet with available options + Double tap to open Action Sheet with available options We cannot open pages at the moment. Please try again later - Selected homepage and page for posts cannot be the same. + Set as Posts Page + Set as Homepage %1$s is not a valid %2$s - Homepage Settings - Choose from a homepage that displays your latest posts (classic blog) or a fixed/static page. - Loading of pages failed - Accept - Cannot save homepage settings - Cannot save homepage settings before pages are loaded + Select Page + Posts Page + Static Homepage + Classic Blog + Selected homepage and page for posts cannot be the same. Homepage settings update failed, check your internet connection - To set Homepage, enable \"Static Homepage\" in Site Settings - To set Posts page, enable \"Static Homepage\" in Site Settings - Homepage successfully updated - Homepage update failed - Posts Page successfully updated + Cannot save homepage settings before pages are loaded + Cannot save homepage settings + Accept + Loading of pages failed + Choose from a homepage that displays your latest posts (classic blog) or a fixed/static page. + Homepage Settings Homepage Posts Page update failed + Posts Page successfully updated + Homepage update failed + Homepage successfully updated + To set Posts page, enable \"Static Homepage\" in Site Settings + To set Homepage, enable \"Static Homepage\" in Site Settings Select a colour - When you follow sites, you\'ll see their content here Double tap to go to colour settings + When you follow sites, you\'ll see their content here Find out more + What\'s New In %s Insert %d crop Failed to load into file, please try again. @@ -1682,7 +1715,6 @@ Language: en_GB Choose media Choose video Couldn\'t select site. Please try again. - What\'s New In %s Continue Reblog failed Manage Sites @@ -1703,16 +1735,15 @@ Language: en_GB Move block left Double tap to move the block to the right Double tap to move the block to the left + Block settings Creating dashboard Setting up theme Adding site features Grabbing site URL Your site will be ready shortly Hooray!\nAlmost done - Block settings Cancel upload There was a problem handling the request - Sunday Powered by Tenor Choose from Tenor Saturday @@ -1721,15 +1752,16 @@ Language: en_GB Wednesday Tuesday Monday + Sunday Failed to access content of a private site. Some media might be unavailable Accessing content of a private site Failed to crop and save image, please try again. + Failed to load image.\nPlease tap to retry. Preview Image Unknown page format We couldn\'t complete this action, and didn\'t submit this page for review. We couldn\'t complete this action, and didn\'t schedule this page. We couldn\'t complete this action, and didn\'t publish this private page. - Failed to load image.\nPlease tap to retry. We couldn\'t complete this action, and didn\'t publish this page. We couldn\'t submit this page for review, but we\'ll try again later. We couldn\'t schedule this page, but we\'ll try again later. @@ -1748,6 +1780,8 @@ Language: en_GB Uploading page Device is offline. Page saved locally. You\'ve made unsaved changes to this page + Your page is uploading + The page has failed media uploads and has been saved locally Page saved on device Page saved online Select blog for QuickPress shortcut @@ -1756,8 +1790,6 @@ Language: en_GB Light Appearance You recently made changes to this page but didn\'t save them. Choose a version to load:\n\n - Your page is uploading - The page has failed media uploads and has been saved locally Warning message Show post content Only show excerpt @@ -1773,8 +1805,8 @@ Language: en_GB Binned Scheduled Published - Not Connected The Facebook connection cannot find any Pages. Jetpack Social cannot connect to Facebook Profiles, only published Pages. + Not Connected Likes Follows Comments @@ -1790,19 +1822,19 @@ Language: en_GB Select a Tag or Site, Pop Up Window Select a Site or Tag to filter posts Remove the current filter - Log in to WordPress.com Manage Topics and Sites - Log in to WordPress.com to see the latest posts from sites you follow + Log in to WordPress.com Log in to WordPress.com to see the latest posts from topics you follow + Log in to WordPress.com to see the latest posts from sites you follow Replace Current Block Add To End Add To Beginning Add Block Before Add Block After + Add a topic Follow a site - See the newest posts from sites you follow You can follow posts on a specific subject by adding a topic - Add a topic + See the newest posts from sites you follow Following Filter Video caption. %s @@ -1847,30 +1879,30 @@ Language: en_GB We were unable to access the <b>XMLRPC file</b> on your site. You will need to reach out to your host to resolve this. Almost there! We just need to verify your Jetpack connected email address <b>%1$s</b> Log in with your %1$s site credentials - Following Site page - We cannot open the posts right now. Please try again later - %sB - %sM - %sQa - %sT - %sQi - Sites - Saved - Discover + Following Likes - We cannot load the data for your site right now. Please try again later + Discover + Saved Topics + Sites + %sQi + %sQa + %sT + %sB + %sM %sK + We cannot open the posts right now. Please try again later + We cannot load the data for your site right now. Please try again later WordPress Media Library Unsupported Ungroup Tap to hide the keyboard Tap here to show help - Take a Photo or Video - Start writing… Take a Video + Take a Photo or Video Take a Photo + Start writing… %s block. This block has invalid content %s block. Empty Cut block @@ -1887,12 +1919,12 @@ Language: en_GB Move block up Move block down from row %1$s to row %2$s Move block down + Link text Link inserted Image caption. %s Hide keyboard Help icon Double tap to undo last change - Link text Double tap to toggle setting Double tap to select an image Double tap to select a video @@ -1907,22 +1939,22 @@ Language: en_GB Choose from device An unknown error occurred. Please try again. Alt Text + Add video Add URL - ADD BLOCK HERE Add alt text + ADD BLOCK HERE Add description - Add video Tap the Add to Save Posts button to save a post to your list. The list has loaded with %1$d items. Notifications - Turning off Notifications for this site will disable the notifications display on the notifications tab for this site. You can fine-tune which kind of notification you see after turning on Notifications for this site. - On Off - Notifications for this site - Notifications for this site + On + Turning off Notifications for this site will disable the notifications display on the notifications tab for this site. You can fine-tune which kind of notification you see after turning on Notifications for this site. To see notifications on the notifications tab for this site, turn on Notifications for this site. - Disable the notifications display on the notifications tab for this site Enable the notifications display on the notifications tab for this site + Disable the notifications display on the notifications tab for this site + Notifications for this site + Notifications for this site Add image or video We couldn\'t submit this post for review, but we\'ll try again later. We couldn\'t schedule this post, but we\'ll try again later. @@ -1949,11 +1981,11 @@ Language: en_GB You\'ve made unsaved changes to this post The version from this app The version from another device + From this app\nSaved on %1$s\n\nFrom another device\nSaved on %2$s\n You recently made changes to this post but didn\'t save them. Choose a version to load:\n\n Which version would you like to edit? Delete permanently We won\'t save the latest changes to your draft. - From this app\nSaved on %1$s\n\nFrom another device\nSaved on %2$s\n We won\'t schedule these changes. We won\'t submit these changes for review. We won\'t publish these changes. @@ -1999,9 +2031,9 @@ Language: en_GB Share Go back Go forward + \"%1$s\" scheduled for publishing on \"%2$s\" in your %3$s app \n %4$s WordPress Scheduled Post: \"%s\" \"%s\" will be published in 10 minutes - \"%1$s\" scheduled for publishing on \"%2$s\" in your %3$s app \n %4$s \"%s\" will be published in 1 hour \"%s\" has been published Scheduled post: 10 minute reminder @@ -2079,6 +2111,7 @@ Language: en_GB Registering domain name… Select State Select Country + Register domain Postcode State City @@ -2087,7 +2120,6 @@ Language: en_GB Country Country code Phone - Register domain Organisation (optional) For your convenience, we have prefilled your WordPress.com\n contact information. Please review to be sure it’s the correct information you want to use for this domain. Domain contact information @@ -2096,27 +2128,27 @@ Language: en_GB Domain owners have to share contact information in a public database of all domains.\n With Privacy Protection, we publish our own information instead of yours and privately forward any communication to you. Privacy Protection Please enter a valid %s + New Dismiss Try it now Choose what stats to see, and focus on the data you care most about. Tap %1$s at the bottom of Insights to customise your stats. Manage Your Stats - New Fetching revisions… Failed to insert media.\nPlease tap for options. Failed to insert media.\nPlease tap to retry. Your draft is uploading Uploading draft - An error occurred while restoring the post Drafts + An error occurred while restoring the post Backdated for: %s Only see the most relevant stats. Add and organise your insights below. Social Annual Site Stats + Follower totals Domain suggestions couldn\'t be loaded Type a keyword for more ideas No suggestions found Register Domain - Follower totals Remove from insights Move down Move up @@ -2125,32 +2157,33 @@ Language: en_GB Post is being restored Post restored Post is being binned - Local changes Binning this post will also discard unsaved changes, are you sure you want to continue? + Local changes + Move to Draft + Switch to list view Switch to cards view You don\'t have any binned posts You don\'t have any draft posts You don\'t have any scheduled posts You haven\'t published any posts yet - Move to Draft - Switch to list view Please log in with your username and password. Please log in using your WordPress.com username instead of your e-mail address. + Avg. words/post Total words + Avg. likes/post Total likes + Avg. comments/post Total comments Posts Year This Year The site at this address is not a WordPress site. For us to connect to it, the site must use WordPress. - Avg. words/post - Avg. likes/post - Avg. comments/post Failed to check available domain credits Checking domain credits Register domain To install plugins, you need to have a custom domain associated with your site. Install plugin + You\'ll be able to customise the look and feel of your site later Publish on: %s Schedule for: %s Published on: %s @@ -2161,7 +2194,6 @@ Language: en_GB Period Months and Years Load more - You\'ll be able to customise the look and feel of your site later Today Best Hour Best Day @@ -2182,20 +2214,20 @@ Language: en_GB We cannot load Plans at the moment. Please try again later. Cannot load Plans No connection + Switch to block editor There was a problem loading your data, refresh your page to try again. Data not loaded Edit new posts and pages with the Block Editor Use Block Editor - Switch to block editor exit - Next Steps - Your visitors will see your icon in their browser. Add a custom icon for a polished, pro look. - Customise your site Grow your audience + Customise your site + Next Steps Choose a unique site icon + Your visitors will see your icon in their browser. Add a custom icon for a polished, pro look. + Select %1$s Stats %2$s to see how your site is performing. Tap %1$s Your Site Icon %2$s to upload a new one Draft and publish a post. - Select %1$s Stats %2$s to see how your site is performing. Enable post sharing Automatically share new posts to your social media accounts. Check your site stats @@ -2204,9 +2236,9 @@ Language: en_GB Reminder Select next period Select previous period + %1$s of views Most Popular Time %1$s (%2$s) - %1$s of views +%1$s (%2$s) Showing site preview Clear @@ -2235,9 +2267,9 @@ Language: en_GB Updating post Discard Web Discard Local + Local\nSaved on %1$s\n\nWeb\nSaved on %2$s\n This post has two versions that are in conflict. Select the version you would like to discard.\n\n Resolve sync conflict - Local\nSaved on %1$s\n\nWeb\nSaved on %2$s\n No data for this period Remove location from media We cannot open the statistics at the moment. Please try again later @@ -2272,8 +2304,8 @@ Language: en_GB Share post Create post It’s been %1$s since %2$s was published. Here’s how the post performed so far: - Tags and Categories It\'s been %1$s since %2$s was published. Get the ball rolling and increase your post views by sharing your post: + Tags and Categories All-time %1$s - %2$s Followers @@ -2324,64 +2356,64 @@ Language: en_GB Medium Thumbnail History - Pending review The selected page is not available - Delete Permanently - Move to Draft - Move to Bin - No pages matching your search - Search pages - You don\'t have any draft pages - You don\'t have any scheduled pages + Pending review You don\'t have any binned pages + You don\'t have any scheduled pages + You don\'t have any draft pages You haven\'t published any pages yet - Drafts - Published - Scheduled + Search pages + No pages matching your search + Delete Permanently + Move to Bin + Move to Draft Set parent - Binned View + Binned + Scheduled + Drafts + Published We\'ve made too many attempts to send an SMS verification code — take a break, and request a new one in a minute. - No sites matching your search + There\'s no WordPress.com account matching this Google account. No sites matching your search + No sites matching your search Page parent has been changed - There\'s no WordPress.com account matching this Google account. - Are you sure you want to delete page %s? - Page has been moved to Drafts Page has been permanently deleted - Page has been published Page has been scheduled + Page has been published Page has been binned - Set Parent + Page has been moved to Drafts + Top level + Are you sure you want to delete page %s? There was a problem changing the page parent There was a problem changing the page status There was a problem deleting the page - Top level + Set Parent Dismiss tap here Create your site Get your site up and running. Doesn\'t it feel good to cross things off a list? - Connect to your social media accounts – your site will automatically share new posts. - Share your site View your site - Tap the %1$s Connections %2$s to add your social media accounts Preview your site to see what your visitors will see. + Share your site Tap %1$s Social %2$s to continue + Tap the %1$s Connections %2$s to add your social media accounts + Connect to your social media accounts – your site will automatically share new posts. Publish a post Tap %1$s Create Post %2$s to create a new post No thanks Connect with other sites - Cancel Go + Cancel Not now - You don\'t have any sites More - Add topics here to find posts about your favourite topics + You don\'t have any sites 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. Jetpack Jetpack FAQ - Log in to the WordPress.com account you used to connect 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? @@ -2392,45 +2424,45 @@ Language: en_GB No media matching your search Log out of WordPress? You have changes to posts that haven’t been uploaded to your site. Logging out now will delete those changes from your device. Log out anyway? + No viewers yet No email followers yet No followers yet No users yet - No viewers yet Posts that you like will appear here - Discover sites + Nothing liked yet Go to following + Discover sites No followed sites - Nothing liked yet - No followers yet No likes yet + No followers yet Since you\'re on a free plan, you\'ll see limited events in your activity. - No activity yet When you make changes to your site you\'ll be able to see your activity history here - Create a page + No activity yet Create a post + Create a page Upload media You don\'t have any media - featured image image gallery site icon theme image + featured image Discard profile picture - Contact email + Transient Email - New message from \'Help & Support\' - Not set Please enter your email address To continue please enter your email address and name - Transient + New message from \'Help & Support\' WordPress - Restoring to %1$s %2$s + Not set + Contact email Restore in progress - Activity Log action button + Restoring to %1$s %2$s Currently restoring your site Your site has been successfully restored - Your site is being restored\nRestoring to %1$s %2$s Your site has been successfully restored\nRestored to %1$s %2$s + Your site is being restored\nRestoring to %1$s %2$s + Activity Log action button Auto-managed Save this post and come back to read it whenever you like. It will only be available on this device — saved posts don\'t sync to your other devices. Save Posts for Later @@ -2445,85 +2477,86 @@ Language: en_GB Magic link login Site address login Email address login - Add to saved posts + Tap %s to save a post to your list. + No posts saved — yet! Post saved + View All Remove from saved posts - Removed + Add to saved posts Saved posts - Tap %s to save a post to your list. - View All - No posts saved — yet! + Removed + Change site icon Cancel + Remove Change - Change site icon - Enable + You don\'t have permission to edit the site icon. + You don\'t have permission to add a site icon. How would you like to edit the icon? - Remove + Would you like to add a site icon? Site Icon this site - Would you like to add a site icon? - You don\'t have permission to add a site icon. - You don\'t have permission to edit the site icon. - Activity icon - Activity Log - Collect information - Cookie Policy + Enable Enable notifications for %1$s%2$s%3$s? - Event + Turn on site notifications + Turn off site notifications Jetpack icon - Privacy Policy - Privacy settings + Event + Activity icon + Activity Log Read privacy policy - Share information with our analytics tool about your use of services while logged in to your WordPress.com account. + We use other tracking tools, including some from third parties. Read about these and how to control them. Third Party Policy This information helps us improve our products, make marketing to you more relevant, personalise your WordPress.com experience, and more as detailed in our privacy policy. - Turn off site notifications - Turn on site notifications - We use other tracking tools, including some from third parties. Read about these and how to control them. + Privacy Policy + Share information with our analytics tool about your use of services while logged in to your WordPress.com account. + Cookie Policy + Privacy settings + Collect information Post submitted - Plugin feature requires primary domain subscription to be associated with this user. Plugin feature requires the site to be in good standing. - Plugin cannot be installed due to disk space limitations. + Plugin feature requires primary domain subscription to be associated with this user. + Plugin feature requires admin privileges. Plugin cannot be installed on VIP sites. - Plugin feature requires a business plan. - Plugin feature requires a custom domain. + Plugin cannot be installed due to disk space limitations. Plugin feature requires a verified email address. - Plugin feature requires admin privileges. Plugin feature requires the site to be public. + Plugin feature requires a business plan. + Plugin feature requires a custom domain. We\'re doing the final setup — almost done… Installing plugin… Install Installing the first plugin on your site can take up to 1 minute. During this time you won’t be able to make changes to your site. - Daily - Email me new comments - Email me new posts Install plugin + Notifications + Email me new comments + Weekly Instantly + Daily New posts - Notifications Receive notifications for new posts from this site - Weekly + Email me new posts All My Followed Sites - Are you sure you\'d like to permanently delete this post? Followed Sites - People looking at graphs and charts Person reading device with notifications + People looking at graphs and charts %1$s on %2$s - General + Are you sure you\'d like to permanently delete this post? Important + General Use this photo %1$d of %2$d - %1$s of unlimited - Add %d - Can\'t save an empty draft - Choose from Free Photo Library Photos provided by %s - Preview %d - Search free photo library Search to find free photos to add to your Media Library + Search free photo library + Choose from Free Photo Library + Can\'t save an empty draft + %1$s of unlimited + Preview %d + Add %d Create Tag navigate up Notifications + Open external link show more photo delete @@ -2547,7 +2580,6 @@ Language: en_GB %s\'s profile picture check mark Signing up with Google… - Open external link Connection to Jetpack failed: %s You are already connected to Jetpack Visual Mode @@ -2576,22 +2608,22 @@ Language: en_GB Edit Photo Pick site New account - Sharing buttons Logged in as Person detail File details + Sharing buttons Notifications Reader Me - Notification settings My Site + Notification settings Your avatar has been uploaded and will be available shortly. 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. Permissions Featured - Version %s - Social module disabled You cannot access your social sharing settings because your Jetpack Social module is disabled. + Social module disabled + Version %s The chosen sound has invalid path. Please choose another. QP %s %1$d pages / posts remaining @@ -2643,15 +2675,15 @@ Language: en_GB Sending email Retry Close + There was some trouble sending the email. You can retry now or close and try again later. Username + You can always log in with a link like the one you just used, but you can also set up a password if you prefer. Password (optional) Display Name Retry Revert There was some trouble updating your account. You can retry or revert your changes to continue. There was some trouble uploading your avatar. - There was some trouble sending the email. You can retry now or close and try again later. - You can always log in with a link like the one you just used, but you can also set up a password if you prefer. Needs update Search Plugins New @@ -2673,46 +2705,46 @@ Language: en_GB Your WordPress Version Requires WordPress Version Last Updated - 1 stars - 2 stars - 3 stars - 4 stars - 5 stars Version + 5 stars + 4 stars + 3 stars + 2 stars + 1 stars + None provided %s downloads %s ratings - Frequently Asked Questions - None provided Read Reviews - Description - Installation + Frequently Asked Questions What\'s New - Installed + Installation + Description Settings + Installed Version %s installed - by %s Version %s + by %s Change photo Unable to load plugins - Deleting + Pages Manage your site\'s tags Saving + Deleting Permanently delete \'%s\' tag? - Pages A tag with this name already exists Add New Tag Description Tag Your WordPress.com site supports the use of Accelerated Mobile Pages, a Google-led initiative that dramatically speeds up loading times on mobile devices Accelerated Mobile Pages (AMP) - Learn more about date & time formatting Unable to load timezones - Custom + Learn more about date & time formatting Custom format + Custom Posts per page Choose a city in your timezone - Time Format Timezone + Time Format Date Format Week starts on Tags @@ -2745,8 +2777,8 @@ Language: en_GB Please provide an authentication code to continue. Please double check your password to continue. Login stopped - Login in progress… Please wait while logging in. + Login in progress… Tap to continue. Logged in! Google login could not be started. @@ -2769,8 +2801,8 @@ Language: en_GB Remove this image from the post? Customise File Details - There was some trouble connecting with the Google account. \nMaybe try a different account? + There was some trouble connecting with the Google account. Close To proceed with this Google account, please provide the matching WordPress.com password. This will be asked only once. A network error occurred. Please check your connection and try again. @@ -2816,8 +2848,8 @@ Language: en_GB File Name URL Alt text - Blink light Connect a site + Blink light Vibrate device Choose sound Sights and Sounds @@ -2832,8 +2864,8 @@ Language: en_GB Enable notifications Disable notifications Off - Maximum Video Size On + Maximum Video Size Maximum Image Size There was an error uploading the media in this post: %s. There was an error uploading this post: %s. @@ -2873,16 +2905,16 @@ Language: en_GB Enter the address of the WordPress site you\'d like to connect. Already logged in to WordPress.com Continue - Enter your WordPress.com password. Connect another site + Enter your WordPress.com password. Requesting log-in email It looks like this password is incorrect. Please double check your information and try again. Requesting a verification code via SMS. Text me a code instead Almost there! Please enter the verification code for WordPress.com from your authenticator app. + Open Mail Next Log in to WordPress.com using an email address to manage all your WordPress sites. - Open Mail Profile Photo Unexpected response from server Can\'t stop the upload because it\'s already finished @@ -2931,8 +2963,8 @@ Language: en_GB %1$s was denied access to your media files. 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 Resizes videos in posts to this size + Enable to resize and compress videos Optimise Videos Draft uploaded Video Quality @@ -2978,8 +3010,8 @@ Language: en_GB Notifications. Manage your notifications. Reader. Follow content from other sites. My Site. View your site and manage it, including stats. - Not now Social + Not now Upload error. Try changing Optimise Images in your app\'s settings Saving media to this device Unable to save media @@ -3019,12 +3051,16 @@ Language: en_GB Post saved online Quality of pictures. Higher values mean better quality pictures. Enable to resize and compress pictures + Maximum + High + Medium + Low Uploaded + Upload Failed Deleted Deleting Uploading Queued - Upload Failed Image Quality All media uploads have been cancelled due to an unknown error. Please retry uploading Unknown post format @@ -3084,46 +3120,46 @@ Language: en_GB Comment approved! Like now - Follower Viewer + Follower No connection, couldn\'t save your profile - None - Left Right + Left + None Selected %1$d Couldn\'t retrieve site users Email Follower Follower Fetching users… - Email Followers Viewers + Email Followers Followers Team Invite up to 10 email addresses and/or WordPress.com usernames. Those needing a username will be sent instructions on how to create one. If you remove this viewer, he or she will not be able to visit this site.\n\nWould you still like to remove this viewer? If removed, this follower will stop receiving notifications about this site, unless they re-follow.\n\nWould you still like to remove this follower? Since %1$s - Couldn\'t remove follower Couldn\'t remove viewer + Couldn\'t remove follower Couldn\'t retrieve site email followers Couldn\'t retrieve site followers Some media uploads have failed. You can\'t switch to HTML mode\n in this state. Remove all failed uploads and continue? - Visual editor Image thumbnail - Changes saved - Caption - Alt text - Link to + Visual editor Width + Link to + Alt text + Caption + Changes saved Discard unsaved changes? Stop uploading? An error occurred while inserting media You are currently uploading media. Please wait until this completes. Can\'t insert media directly in HTML mode. Please switch back to visual mode. Uploading gallery… - %1$s: %2$s - Invite sent successfully Tap to try again! + Invite sent successfully + %1$s: %2$s Invite sent but error(s) occurred! An error occurred while trying to send the invite! Cannot send: There are invalid usernames or emails @@ -3132,8 +3168,8 @@ Language: en_GB Custom message Invite Usernames or emails - External Invite People + External Clear search history Clear search history? No results found for %s for your language @@ -3141,33 +3177,33 @@ Language: en_GB Related Post Links are disabled on the preview screen Send - If you remove %1$s, that user will no longer be able to access this site, but any content that was created by %1$s will remain on the site.\n\nWould you still like to remove this user? Successfully removed %1$s + If you remove %1$s, that user will no longer be able to access this site, but any content that was created by %1$s will remain on the site.\n\nWould you still like to remove this user? Remove %1$s - The sites in this list haven\'t posted anything recently - People Role + People + The sites in this list haven\'t posted anything recently Couldn\'t remove user Couldn\'t update user role Couldn\'t retrieve site viewers Error updating your Gravatar - Error locating the cropped image Error reloading your Gravatar + Error locating the cropped image Error cropping the image Checking email Currently unavailable. Please enter your password Logging in Shown publicly when you comment. Capture or select photo - Your posts, pages, and settings will be emailed to you at %s. - Plan Plans + Plan + Your posts, pages, and settings will be emailed to you at %s. Export your content - Exporting content… Export email sent! - You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site. - Show purchases + Exporting content… Checking purchases + Show purchases + You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site. Premium Upgrades Something went wrong. Could not request purchases. Deleting site… @@ -3176,15 +3212,15 @@ Language: en_GB Primary Domain There was an error in deleting your site. Please contact support for more assistance. Error deleting site - Please type %1$s in the field below to confirm. Your site will then be gone forever. Export content + Please type %1$s in the field below to confirm. Your site will then be gone forever. Confirm Delete Site Contact Support If you want a site but don\'t want any of the posts and pages you have now, our support team can delete your posts, pages, media and comments for you.\n\nThis will keep your site and URL active, but give you a fresh start on your content creation. Just contact us to have your current content cleared out. - Start your site again Let Us Help - App Settings + Start your site again Start Over + App Settings Remove failed uploads Advanced No binned comments @@ -3192,34 +3228,34 @@ Language: en_GB No approved comments Skip Couldn\'t connect. Required XML-RPC methods are missing on the server. - Status - Video Centre - Chat - Gallery - Image - Link - Quote + Video + Status Standard - Information on WordPress.com courses and events (online & in-person). - Aside + Quote + Link + Image + Gallery + Chat Audio + Aside + Information on WordPress.com courses and events (online & in-person). Opportunities to participate in WordPress.com research & surveys. Tips for getting the most out of WordPress.com. Community - Replies to my comments - Suggestions Research - Site achievements + Suggestions + Replies to my comments Username mentions - Likes on my posts + Site achievements Site follows + Likes on my posts Likes on my comments Comments on my site %d items 1 item - Known users\' comments All users + Known users\' comments No comments %d comments per page 1 comment per page @@ -3229,11 +3265,11 @@ Language: en_GB Automatically approve everyone\'s comments. Automatically approve if the user has a previously approved comment Require manual approval for everyone\'s comments. - 1 day %d days - Click the verification link in the email sent to %1$s to confirm your new address - Primary site + 1 day Web address + Primary site + Click the verification link in the email sent to %1$s to confirm your new address You are currently uploading media. Please wait until this completes. Comments couldn\'t be refreshed at this time - showing older comments Set Featured Image @@ -3242,13 +3278,13 @@ Language: en_GB Permanently delete these comments? Permanently delete this comment? Delete - Comment deleted Restore + Comment deleted No spam comments - Could not load page All - Interface Language + Could not load page Off + Interface Language About the app Couldn\'t save your account settings Couldn\'t retrieve your account settings @@ -3257,8 +3293,8 @@ Language: en_GB Allow comments to be nested in threads. Thread up to Disabled - Remove Search + Remove Original Size Your site is visible only to you and users you approve Your site is visible to everyone but asks search engines not to index it @@ -3267,8 +3303,8 @@ Language: en_GB About me Display name will default to your username if it is not set Public display name - First name Last name + First name My Profile Related post preview image Couldn\'t save site info @@ -3324,8 +3360,8 @@ Language: en_GB %d levels Private Hidden - Delete Site Public + Delete Site Hold for Moderation Links in comments Automatically approve @@ -3340,22 +3376,22 @@ Language: en_GB Default Format Default Category Address - Site Title Tagline + Site Title Defaults for new posts - Account Writing + Account General Newest first + Oldest first Close after Comments - Discussion - Oldest first - Privacy Related Posts + Privacy + Discussion You don\'t have permission to upload media to the site - Never Unknown + Never This post no longer exists You\'re not authorised to view this post Unable to retrieve this post @@ -3367,22 +3403,22 @@ Language: en_GB Something went wrong. Could not activate theme by %1$s Thanks for choosing %1$s - Details - DONE MANAGE SITE + DONE Support - Try & Customise + Details View + Try & Customise Activate Active - Current Theme - Customise - Details Support - Page published + Details + Customise + Current Theme Page updated - Post published Post updated + Page published + Post published Sorry, no themes found. Load more posts No sites matched \'%s\' @@ -3415,200 +3451,200 @@ Language: en_GB Couldn\'t load notification settings Comment likes App notifications - Notifications tab Email + Notifications tab We\'ll always send important emails regarding your account, but you can get some helpful extras, too. Latest Post Summary No connection Post sent to bin - Stats Bin + Stats Preview View - Edit Publish + Edit You are not authorised to access this site This site could not be found Undo The request has expired. Log in to WordPress.com to try again. - Best Views Ever Ignore + Best Views Ever Today\'s Stats All-time posts, views, and visitors Insights Log out of WordPress.com - Login/Logout Log in to WordPress.com + Login/Logout Account Settings \"%s\" wasn\'t hidden because it\'s the current site Create WordPress.com site Add self-hosted site - Show/hide sites Add new site - View Admin - View Site + Show/hide sites Choose site + View Site + View Admin Switch Site - Look and Feel - Publish Site Settings Posts + Publish + Look and Feel Configuration Tap to show them Deselect all - Show - Hide Select all - Language - Verification code - Invalid verification code + Hide + Show Log in again to continue. + Invalid verification code + Verification code + Language Unable to retrieve posts Could not open notification Unknown Search Terms - Authors Search Terms + Authors Fetching pages… Fetching posts… Fetching media… Application logs have been copied to the clipboard + This site is empty New posts An error occurred while copying text to clipboard Uploading post - This site is empty - Fetching themes… - %1$d months - A year %1$d years + A year + %1$d months A month - %1$d minutes - an hour ago - %1$d hours - A day %1$d days + A day + %1$d hours + an hour ago + %1$d minutes a minute ago seconds ago - Posts & Pages - Videos Followers + Videos + Posts & Pages Countries Likes - Years - Views Visitors + Views + Years + Fetching themes… Details %d selected Browse our FAQ No comments yet - View original article + No posts with this topic Like + View original article Comments are closed %1$d of %2$d Can\'t publish an empty post You don\'t have permission to view or edit posts You don\'t have permission to view or edit pages - Older than a month More - Older than 2 days + Older than a month Older than a week + Older than 2 days + Help and support Liked Comment - No posts yet. Why not create one? - Reply to %s Comment binned + Reply to %s + No posts yet. Why not create one? Logging out… - No posts with this topic - Help and support Unable to perform this action - Block this site - Posts from this site will no longer be shown Unable to block this site - Update + Posts from this site will no longer be shown + Block this site Schedule - Followed sites + Update No recommended sites - Reader Site - Site followed - Unable to follow this site - Unable to show this site Unable to unfollow this site + Unable to follow this site You already follow this site - Followed topics - Enter a URL or topic to follow + Unable to show this site + Site followed %s followers - Help - Invalid SSL certificate + Enter a URL or topic to follow + Followed sites + Followed topics + Reader Site If you usually connect to this site without problems, this error could mean that someone is trying to impersonate the site, and you shouldn\'t continue. Would you like to trust the certificate anyway? - There is no network available - The media item couldn\'t be retrieved - An error occurred when accessing this blog - Failed to fetch themes - Not spam - Adding category failed - Category added successfully - The category name field is required - A mounted SD card is required to upload media - No notifications - Posts couldn\'t be refreshed at this time - Pages couldn\'t be refreshed at this time - Comments couldn\'t be refreshed at this time - An error occurred - An error occurred while moderating - An error occurred while editing the comment - Couldn\'t load the comment - Error downloading image - Your email address isn\'t valid - Enter a valid email address + Invalid SSL certificate + Help The username or password you entered is incorrect + Enter a valid email address + Your email address isn\'t valid + Error downloading image + Couldn\'t load the comment + An error occurred while editing the comment + An error occurred while moderating + An error occurred + Comments couldn\'t be refreshed at this time + Pages couldn\'t be refreshed at this time + Posts couldn\'t be refreshed at this time An error occurred while deleting the post - Select categories - Connection error - An error occurred when loading the post. Refresh your posts and try again. - Learn more - Thumbnail grid - You don\'t have permission to view the media library - Some media can\'t be deleted at this time. Try again later. - Approved - Pending - Spam - Edit comment - Approve - Unapprove - Spam - Send to bin? - Link text (optional) - Create a link - Page settings - Local draft - Horizontal alignment - Post settings - Bin - Saving changes - View in browser - Add new category - Category name - Couldn\'t create temp file for media upload. Make sure there is enough free space on your device. - New post - New media - Local changes - Image settings - WordPress blog - This blog is hidden and couldn\'t be loaded. Enable it again in settings and try again. - An error occurred while creating the app database. Try reinstalling the app. + No notifications + A mounted SD card is required to upload media + The category name field is required + Category added successfully + Adding category failed + Not spam + Failed to fetch themes + An error occurred when accessing this blog + The media item couldn\'t be retrieved + There is no network available + Unable to remove this topic + Unable to add this topic Application log - Remove site + An error occurred while creating the app database. Try reinstalling the app. + This blog is hidden and couldn\'t be loaded. Enable it again in settings and try again. + Media couldn\'t be refreshed at this time + WordPress blog + Image settings + Local changes + New media + New post No notifications…yet. Authorisation required + Check that the site URL entered is valid + Couldn\'t create temp file for media upload. Make sure there is enough free space on your device. + Category name + Add new category + View in browser + Remove site + Saving changes + Bin + Send to bin? Bin + Spam + Unapprove + Approve + Edit comment Binned - Check that the site URL entered is valid - Media couldn\'t be refreshed at this time + Spam + Pending + Approved + Delete page? + Delete post? + Post settings Couldn\'t find the file for upload. Was it deleted or moved? + Horizontal alignment + Local draft + Page settings + Create a link + Link text (optional) + Some media can\'t be deleted at this time. Try again later. + You don\'t have permission to view the media library + Thumbnail grid + Learn more + An error occurred when loading the post. Refresh your posts and try again. An error occurred when accessing this plugin - Delete post? - Delete page? - Unable to add this topic - Unable to remove this topic + Connection error + Select categories Share link Fetching posts… You and %,d others like this @@ -3616,75 +3652,75 @@ Language: en_GB You can\'t share to WordPress without a visible blog Comment marked as spam Comment unapproved + Unable to retrieve this post You and one other like this - Select photo Select video - Unable to retrieve this post - This list is empty - (Untitled) - Reblog - Couldn\'t post your comment - Unable to share - Unable to view image + Select photo + Sign Up Unable to open %s - Share - Follow - Following - Added %s - Removed %s - One person likes this + Unable to view image + Unable to share + That isn\'t a valid topic + You already follow this topic + Couldn\'t post your comment You like this + One person likes this + Removed %s + Added %s Reply to comment… + Following + Follow + Share + Reblog + (Untitled) No comments yet - Sign Up - You already follow this topic - That isn\'t a valid topic - Themes - Title - Caption - Description - Squares - Tiled - Circles - Slideshow - Clicks - Referrers - Today - Yesterday - Days - Weeks + This list is empty Months - Failed to update - Activate - Share - Stats + Weeks + Days + Yesterday + Today + Referrers Tags & Categories + Clicks + Stats + Share + Activate + Failed to update + Description + Caption + Title + Slideshow + Circles + Tiled + Squares + Themes Discard Manage - Reply published - %d new notifications and %d more. + %d new notifications Follows + Reply published Log in Loading… - HTTP username HTTP password + HTTP username An error occurred while uploading media Incorrect username or password. - Password - Username Log In + Username + Password Reader - Use as featured image Include image in post content - No network available - Pages - Caption (optional) + Use as featured image Width + Caption (optional) + Pages Posts Anonymous - OK + No network available done + OK URL Uploading… Alignment @@ -3697,27 +3733,27 @@ Language: en_GB Shortcut name can\'t be empty Private Title - Categories Separate tags with commas + Categories SD Card Required Media Category updated successfully - Delete Approve - None + Delete Updating category failed - Error - Cancel - Save - Add - Category refresh error - Preview - on + None + Publish Now Reply - Yes + on + Preview + Category refresh error + Error No + Yes Notification Settings - Publish Now + Add + Save + Cancel Once Twice diff --git a/WordPress/src/main/res/values-es/strings.xml b/WordPress/src/main/res/values-es/strings.xml index 56e760e69bcc..2a3cb58517a2 100644 --- a/WordPress/src/main/res/values-es/strings.xml +++ b/WordPress/src/main/res/values-es/strings.xml @@ -1,11 +1,24 @@ + ¡Vamos allá! + Activa las sugerencias de publicación + Bloganuary utilizará las sugerencias de publicación diarias para enviarte temas durante el mes de enero. + Bloganuary usará las sugerencias diarias de publicación para enviarte temas para el mes de enero. Actualmente tienes desactivadas las sugerencias de publicación. + Lee las respuestas de otros blogueros para conseguir inspiración y hacer nuevas conexiones. + Publica tu respuesta. + Recibe una sugerencia nueva para inspirarte cada día. + Únete a nuestro reto de escritura de un mes + Bloganuary + Durante el mes de enero, recibirás sugerencias de publicación de Bloganuary, nuestro reto de la comunidad para que crees un hábito de publicación sólido para el nuevo año. + ¡Bloganuary está a la vuelta de la esquina! + No, apágala + Sí, déjala encendida Por esta razón, te recomendamos que edites el bloque utilizando tu navegador web. Por este motivo, te recomendamos que edites el bloque utilizando el editor web. También puedes aplanar el contenido desagrupando el bloque. @@ -20,10 +33,8 @@ Language: es Es posible que los bloques anidados a más de %d niveles no se muestren correctamente en el editor móvil. Vamos Es hora de continuar tu viaje por WordPress en la aplicación Jetpack. - La optimización de imágenes las reduce para subirlas más rápido.\n\nEsta opción está activada por defecto, pero puedes cambiarla en los ajustes de la aplicación en cualquier momento. - ¿Seguir optimizando las imágenes? - No, apágala - Sí, déjala encendida + La optimización de imágenes las reduce para subirlas más rápido.\n\nEsta opción está activada por defecto, pero puedes cambiarla en los ajustes de la aplicación en cualquier momento. + ¿Seguir optimizando las imágenes? Vaciar la búsqueda Muy alta Introduce tu clave de seguridad para continuar. @@ -2280,7 +2291,7 @@ Language: es Enlace Clics Vistas - Referente + Remitente Referentes Entradas y páginas Ruta diff --git a/WordPress/src/main/res/values-fr-rCA/strings.xml b/WordPress/src/main/res/values-fr-rCA/strings.xml index 9f11a78039c6..688d798ff046 100644 --- a/WordPress/src/main/res/values-fr-rCA/strings.xml +++ b/WordPress/src/main/res/values-fr-rCA/strings.xml @@ -1,11 +1,13 @@ + Non, désactiver + Oui, laisser activé Pour cette raison, nous vous recommandons de modifier le bloc à l’aide de votre navigateur web. Pour cette raison, nous vous recommandons de modifier le bloc à l’aide de l’éditeur web. Sinon, vous pouvez aplatir le contenu en dégroupant le bloc. @@ -20,10 +22,6 @@ Language: fr Les blocs imbriqués à un niveau supérieur à %d peuvent ne pas s’afficher correctement dans l’éditeur mobile. C’est parti Il est temps de poursuivre votre aventure WordPress sur l’application Jetpack. - L’optimisation des images réduit leur taille pour un chargement plus rapide.\n\nCette option est activée par défaut, mais vous pouvez la modifier à tout moment dans les réglages de l’application. - Continuer à optimiser les images ? - Non, désactiver - Oui, laisser activé Supprimer la recherche Très élevée Veuillez fournir votre clé de sécurité pour continuer. diff --git a/WordPress/src/main/res/values-fr/strings.xml b/WordPress/src/main/res/values-fr/strings.xml index 9f11a78039c6..688d798ff046 100644 --- a/WordPress/src/main/res/values-fr/strings.xml +++ b/WordPress/src/main/res/values-fr/strings.xml @@ -1,11 +1,13 @@ + Non, désactiver + Oui, laisser activé Pour cette raison, nous vous recommandons de modifier le bloc à l’aide de votre navigateur web. Pour cette raison, nous vous recommandons de modifier le bloc à l’aide de l’éditeur web. Sinon, vous pouvez aplatir le contenu en dégroupant le bloc. @@ -20,10 +22,6 @@ Language: fr Les blocs imbriqués à un niveau supérieur à %d peuvent ne pas s’afficher correctement dans l’éditeur mobile. C’est parti Il est temps de poursuivre votre aventure WordPress sur l’application Jetpack. - L’optimisation des images réduit leur taille pour un chargement plus rapide.\n\nCette option est activée par défaut, mais vous pouvez la modifier à tout moment dans les réglages de l’application. - Continuer à optimiser les images ? - Non, désactiver - Oui, laisser activé Supprimer la recherche Très élevée Veuillez fournir votre clé de sécurité pour continuer. diff --git a/WordPress/src/main/res/values-gl/strings.xml b/WordPress/src/main/res/values-gl/strings.xml index f233a798e004..23a49988bc92 100644 --- a/WordPress/src/main/res/values-gl/strings.xml +++ b/WordPress/src/main/res/values-gl/strings.xml @@ -1,6 +1,6 @@ + לא, נא לכבות + כן, להמשיך + מסיבה זו מומלץ \'לשטח\' את התוכן על ידי ביטול הקבצת הבלוק או לערוך את הבלוק בעזרת דפדפן האינטרנט. + מסיבה זו מומלץ \'לשטח\' את התוכן על ידי ביטול הקבצת הבלוק או לערוך את הבלוק בעזרת עורך האינטרנט. + Alternatively, you can flatten the content by ungrouping the block. + לעבור להגדרות + ביטול + אישור + דחית את הרשאות המצלמה לצמיתות. ההרשאה נדרשת כדי לסרוק את הברקוד. יש להפעיל את ההרשאה מההגדרות של האפליקציה + ההרשאה למצלמה נדרשת כדי לסרוק את הברקוד + לאשר הרשאות מצלמה + נדרשת הרשאה למצלמה כדי לסרוק את הברקוד. + לסרוק את הברקוד + בעורך לנייד ייתכן עיבוד לא תקין של בלוקים בקינון עמוק מ-%d רמות. + קדימה + It\'s time to continue your WordPress journey on the Jetpack app. + מיטוב התמונות מכווץ תמונות להעלאה מואצת.\n\nאפשרות זאת מופעלת כברירת מחדל, אך ניתן לשנות אותה בכל עת דרך הגדרות האפליקציה. + האם להמשיך במיטוב תמונות? + לנקות את החיפוש + גבוהה מאוד כדי להמשיך עליך לספק מפתח אבטחה. בעיה בהתחברות בעזרת מפתח האבטחה להשתמש במפתח אבטחה @@ -3010,6 +3030,9 @@ Language: he_IL הפוסט נשמר ברשת איכות תמונות. ערכים גבוהים יותר מאפשרים איכות טובה יותר של התמונות. הפעלת עריכה של גודל תמונות ודחיסתן + High + בינוני + נמוכים הועלה העלאה נכשלה נמחק diff --git a/WordPress/src/main/res/values-id/strings.xml b/WordPress/src/main/res/values-id/strings.xml index 464a182e7853..a6da24699c36 100644 --- a/WordPress/src/main/res/values-id/strings.xml +++ b/WordPress/src/main/res/values-id/strings.xml @@ -1,11 +1,13 @@ + Tidak, matikan + Ya, biarkan Karenanya, kami menyarankan Anda untuk mengedit blok melalui browser web. Karenanya, kami menyarankan Anda untuk mengedit blok melalui editor web. Atau, Anda dapat meratakan konten dengan membatalkan pengelompokan blok. @@ -20,10 +22,7 @@ Language: id Blok bertingkat yang kedalamannya melebihi %d level mungkin tidak dapat ditampilkan dengan benar di editor seluler. Ayo Waktunya untuk melanjutkan perjalanan WordPress Anda di aplikasi Jetpack! - Pengoptimalan gambar mengecilkan gambar agar bisa diunggah lebih cepat.\n\nPilihan ini diaktifkan sesuai dengan pengaturan asal, tetapi Anda dapat mengubahnya di pengaturan aplikasi kapan saja. - Teruskan pengoptimalan gambar? - Tidak, matikan - Ya, biarkan + Pengoptimalan gambar mengecilkan gambar agar bisa diunggah lebih cepat.\n\nPilihan ini diaktifkan secara default, tetapi Anda dapat mengubahnya di pengaturan aplikasi kapan saja. Hapus pencarian Sangat Tinggi Silakan berikan kunci keamanan Anda untuk melanjutkan. diff --git a/WordPress/src/main/res/values-it/strings.xml b/WordPress/src/main/res/values-it/strings.xml index 4d2da14a7b44..2bb6eb2c0ac7 100644 --- a/WordPress/src/main/res/values-it/strings.xml +++ b/WordPress/src/main/res/values-it/strings.xml @@ -1,11 +1,13 @@ + No, disattiva + Si, continua Per questo motivo consigliamo di modificare il blocco tramite il browser web. Per questo motivo consigliamo di modificare il blocco tramite l\'editor web. In alternativa, puoi uniformare i contenuti separando il blocco. @@ -20,10 +22,6 @@ Language: it I blocchi con più di %d livelli di nidificazione potrebbero non essere visualizzati correttamente nell\'editor per dispositivi mobili. Iniziamo È ora di continuare il tuo viaggio con WordPress sull\'app Jetpack. - L\'ottimizzazione delle immagini restringe l\'immagine per un caricamento più veloce.\n\nQuesta opzione viene attivata per impostazione predefinita, ma puoi modificarla nelle impostazioni dell\'app in qualsiasi momento. - Vuoi continuare a ottimizzare le immagini? - No, disattiva - Si, continua Cancella ricerca Molto alta Fornisci una chiave di sicurezza per continuare. diff --git a/WordPress/src/main/res/values-ja/strings.xml b/WordPress/src/main/res/values-ja/strings.xml index a19bd339d1e5..f58204dc3f3a 100644 --- a/WordPress/src/main/res/values-ja/strings.xml +++ b/WordPress/src/main/res/values-ja/strings.xml @@ -1,11 +1,13 @@ + オフにする + そのままにする このため、Web ブラウザーでブロックを編集することをお勧めします。 このため、Web エディターでブロックを編集することをお勧めします。 あるいは、ブロックのグループ化を解除してコンテンツをフラットにすることもできます。 @@ -20,10 +22,6 @@ Language: ja_JP %dレベルよりも深くネストされたブロックは、モバイルエディターで適切にレンダリングされない場合があります。 スタート Jetpack アプリで WordPress のジャーニーを続ける時が来ました。 - 画像の最適化により、画像のサイズを縮小して迅速に読み込めます。\n\nこのオプションはデフォルトで有効になっていますが、アプリの設定でいつでも変更できます。 - 画像の最適化を続けますか ? - オフにする - そのままにする 検索をクリア 非常に高い 続行するにはセキュリティキーを入力してください。 diff --git a/WordPress/src/main/res/values-ko/strings.xml b/WordPress/src/main/res/values-ko/strings.xml index e0436cae5390..69e9cfef433d 100644 --- a/WordPress/src/main/res/values-ko/strings.xml +++ b/WordPress/src/main/res/values-ko/strings.xml @@ -1,11 +1,13 @@ + 아니요. 끄기 + 예. 그대로 두기 따라서 웹 브라우저를 사용하여 블록을 편집하는 것이 좋습니다. 따라서 웹 편집기를 사용하여 블록을 편집하는 것이 좋습니다. 그 대신에 블록 그룹 해제를 통해 콘텐츠를 평평하게 하실 수 있습니다. @@ -20,10 +22,6 @@ Language: ko_KR %d 레벨보다 깊숙이 중첩된 블록은 모바일 편집기에서 올바르게 렌더링되지 않을 수 있습니다. 지금 시작 젯팩 앱에서 워드프레스 여정을 계속하실 때가 되었습니다. - 이미지 최적화에서는 더 빠른 업로드를 위해 이미지가 축소됩니다.\n\n이 옵션은 기본적으로 활성화되어 있지만, 언제든지 앱 설정에서 변경하실 수 있습니다. - 이미지를 계속 최적화하시겠어요? - 아니요. 끄기 - 예. 그대로 두기 검색 지우기 매우 높음 계속하려면 보안 키를 입력하세요. diff --git a/WordPress/src/main/res/values-nl/strings.xml b/WordPress/src/main/res/values-nl/strings.xml index db7e1440097d..5a7005418d49 100644 --- a/WordPress/src/main/res/values-nl/strings.xml +++ b/WordPress/src/main/res/values-nl/strings.xml @@ -1,11 +1,28 @@ + Nee, uitschakelen + Ja, ingeschakeld laten + Hierom raden we aan de content platter te maken door het blok niet te groeperen of het blok te bewerken in je webbrowser. + Hierom raden we aan de content platter te maken door het blok niet te groeperen of het blok te bewerken in je webbrowser. + Alternatively, you can flatten the content by ungrouping the block. + Toekennen + Je hebt de cameramachtiging permanent geweigerd. Deze is nodig om de streepjescode te scannen. Schakel deze in de appinstellingen in + De cameramachtiging is vereist om de streepjescode te scannen. + Cameramachtiging toekennen + De cameramachtiging is vereist om de streepjescode te scannen. + Streepjescode scannen + Blokken die dieper dan %d niveaus genesteld zijn worden mogelijk niet goed weergegeven in de mobiele editor. + Aan de slag + It\'s time to continue your WordPress journey on the Jetpack app. + Afbeeldingoptimalisatie maakt afbeeldingen kleiner, zodat ze sneller geüpload kunnen worden.\n\nDeze optie staat standaard ingeschakeld, maar je kan dit altijd veranderen in de app-instellingen. + Afbeeldingen blijven optimaliseren? + Zeer hoog Voer je beveiligingssleutel in om door te gaan. Er heeft zich een probleem met de aanmelding via de beveiligingssleutel voorgedaan. Een beveiligingssleutel gebruiken @@ -55,6 +72,7 @@ Language: nl Ingeplande berichten Conceptberichten Weergaven, bezoekers en likes + Startpagina personaliseren Tik om je startpagina te personaliseren Personaliseer je startpagina Wijzig instellingen diff --git a/WordPress/src/main/res/values-ro/strings.xml b/WordPress/src/main/res/values-ro/strings.xml index 17a6a91eaa06..6c2c79609835 100644 --- a/WordPress/src/main/res/values-ro/strings.xml +++ b/WordPress/src/main/res/values-ro/strings.xml @@ -1,11 +1,24 @@ + Să începem! + Activează îndemnurile de a scrie + Bloganuary va folosi îndemnuri zilnice de a scrie ca să-ți trimită subiecte în luna ianuarie. + Bloganuary va folosi îndemnuri zilnice de a scrie ca să-ți trimită subiecte în luna ianuarie. Acum, îndemnurile de a scrie sunt dezactivate. + Citești răspunsurile altor bloggeri ca să găsești inspirație și să faci conexiuni noi. + Publică răspunsul tău. + Primești în fiecare zi un îndemn nou, ca să te inspire. + Alătură-te provocării noastre de a scrie timp de o lună + Bloganuary + În luna ianuarie, îndemnurile de a scrie vor veni de la Bloganuary - provocarea comunității noastre de a crea un obicei de a scrie pe bloguri în noul an. + Bloganuary vine în curând! + Nu, oprește + Da, lasă în continuare Din acest motiv, recomandăm să editezi blocul folosind navigatorul web. Din acest motiv, recomandăm să editezi blocul folosind editorul web. Ca alternativă, poți să aplatizezi conținutul prin anularea grupării blocurilor. @@ -20,10 +33,8 @@ Language: ro Este posibil ca blocurile imbricate mai adânc de %d niveluri să nu fie randate corect în editorul aplicației pentru mobil. Să-i dăm drumul Este timpul să-ți continui călătoria WordPress în aplicația Jetpack. - Optimizarea imaginilor micșorează imaginile pentru o încărcare mai rapidă.\n\nAceastă opțiune este activată implicit, dar poți să o modifici oricând în setările aplicației. - Continui optimizarea imaginilor? - Nu, oprește - Da, lasă în continuare + Optimizarea imaginilor micșorează imaginile pentru o încărcare mai rapidă.\n\nAceastă opțiune este activată implicit, dar poți să o modifici oricând în setările aplicației. + Continui optimizarea imaginilor? Șterge căutarea Foarte mare Pentru a continua, trebuie să furnizezi cheia de securitate. diff --git a/WordPress/src/main/res/values-ru/strings.xml b/WordPress/src/main/res/values-ru/strings.xml index 51790eb77f42..480a8fc4ea90 100644 --- a/WordPress/src/main/res/values-ru/strings.xml +++ b/WordPress/src/main/res/values-ru/strings.xml @@ -1,11 +1,13 @@ + Нет, отключить + Да, оставить По этой причине мы рекомендуем редактировать блок с помощью веб-браузера. По этой причине мы рекомендуем редактировать блок с помощью веб-редактора. Альтернативно вы можете сгладить содержимое, разгруппировав блок. @@ -20,10 +22,8 @@ Language: ru Блоки, вложенные глубже, чем %d уровней, могут отображаться неправильно в мобильном редакторе. Поехали! Пришло время продолжить путешествие по WordPress в приложении Jetpack! - Оптимизация изображений сжимает изображения для более быстрой загрузки.\n\nЭта опция включена по умолчанию, но вы можете изменить ее в настройках приложения в любой момент. - Продолжать оптимизировать изображения? - Нет, отключить - Да, оставить + Оптимизация изображений сжимает изображения для более быстрой загрузки.\n\nЭта опция включена по умолчанию, но вы можете изменить ее в настройках приложения в любой момент. + Продолжать оптимизировать изображения? Очистить поиск Очень высокое Пожалуйста предоставьте ключ безопасности для продолжения. diff --git a/WordPress/src/main/res/values-sq/strings.xml b/WordPress/src/main/res/values-sq/strings.xml index 65e27d6121da..830094c9f5c8 100644 --- a/WordPress/src/main/res/values-sq/strings.xml +++ b/WordPress/src/main/res/values-sq/strings.xml @@ -1,11 +1,20 @@ + Jo, çaktivizoje + Po, lëre të aktivizuar + Për këtë arsye, rekomandojmë përpunimin e bllokut duke përdorur shfletuesin tuaj. + Për këtë arsye, rekomandojmë përpunimin e bllokut duke përdorur përpunuesin web. + Ndryshe, mund ta sheshoni lëndën duke hequr bllokun nga grupi. + Kaloni te rregullimet + Anuloje + Akordoje + Keni mohuar përgjithmonë leje Kamerës. Është e domosdoshme për të skanuar kodin me vija. Ju lutemi, aktivizojeni që prej rregullimeve të aplikacionit. Ju lutemi, që të vazhdohet, jepni kyçin tuaj të sigurisë. Pati ca problem me hyrjen me kyç sigurie Përdorni një kyç sigurie @@ -392,6 +401,7 @@ Language: sq_AL ✓ Me Përgjigje Cytje mbylle + Ndryshe, mund ta shkëputni dhe përpunoni ndarazi këtë bllok, duke prekur “Shkëpute”. Të fshihet përgjithmonë Kategoria \'%s\'? Kategoria u fshi me sukses Fshirja e kategorisëdështoi @@ -3011,6 +3021,10 @@ Language: sq_AL Postimi u ruajt në internet Cilësi fotosh. Vlera më të mëdha do të thotë cilësi më e lartë fotosh. Aktivizojeni që të ripërmasoni dhe ngjeshni foto + Maksimum + E lartë + Mesatare + E ulët U ngarkua Ngarkimi Dështoi U fshi diff --git a/WordPress/src/main/res/values-sv/strings.xml b/WordPress/src/main/res/values-sv/strings.xml index a822b645be19..c175157ed2c1 100644 --- a/WordPress/src/main/res/values-sv/strings.xml +++ b/WordPress/src/main/res/values-sv/strings.xml @@ -1,11 +1,13 @@ + Nej, stäng av + Ja, lämna på Av den här anledningen rekommenderar vi att du redigerar blocket i din webbläsare. Av den här anledningen rekommenderar vi att du redigerar blocket med webbredigeraren. Alternativt kan du platta ut innehållet genom att avgruppera blocket. @@ -20,10 +22,8 @@ Language: sv_SE Block som är inbäddade djupare än %d kanske inte renderas korrekt i den mobila redigeraren. Sätt igång Det är dags att fortsätta din WordPress-resa med Jetpack-appen. - Bildoptimering minskar storleken på bilder för snabbare uppladdning.\n\nDet här alternativet är aktiverat som standard, men du kan ändra det i appinställningarna när som helst. - Vill du fortsätta optimera bilder? - Nej, stäng av det - Ja, lämna det på + Bildoptimering förminskar bilder för snabbare uppladdning.\n\nDetta alternativ är aktiverat som standard, men du kan ändra det i appinställningarna när som helst. + Fortsätt optimera bilder? Rensa sökning Mycket hög Ange din säkerhetsnyckel för att fortsätta. diff --git a/WordPress/src/main/res/values-tr/strings.xml b/WordPress/src/main/res/values-tr/strings.xml index 445207950267..d7e96b830300 100644 --- a/WordPress/src/main/res/values-tr/strings.xml +++ b/WordPress/src/main/res/values-tr/strings.xml @@ -1,11 +1,13 @@ + Hayır, kapat + Evet, açık kalsın Bu nedenle, bloku web tarayıcınızı kullanarak düzenlemenizi öneririz. Bu nedenle, bloku web düzenleyicinizi kullanarak düzenlemenizi öneririz. Alternatif olarak bloku gruplamadan çıkararak içeriği düzleştirebilirsiniz. @@ -20,10 +22,6 @@ Language: tr %d seviyelerinden daha derin bir şekilde iç içe geçmiş bloklar, mobil düzenleyicide düzgün şekilde oluşmayabilir. Haydi başlayalım Jetpack uygulamasındaki WordPress yolculuğunuza devam etme vakti. - Görsel iyileştirme özelliği, daha hızlı yüklenmeleri için görselleri küçültür.\n\nBu seçenek varsayılan olarak etkindir, ancak dilediğiniz zaman uygulama ayarlarından değiştirebilirsiniz. - Görseller iyileştirilmeye devam edilsin mi? - Hayır, kapat - Evet, açık kalsın Aramayı temizle Çok Yüksek Lütfen devam etmek için güvenlik anahtarınızı girin. diff --git a/WordPress/src/main/res/values-zh-rCN/strings.xml b/WordPress/src/main/res/values-zh-rCN/strings.xml index 2a6152399d60..21bfdd38523d 100644 --- a/WordPress/src/main/res/values-zh-rCN/strings.xml +++ b/WordPress/src/main/res/values-zh-rCN/strings.xml @@ -1,11 +1,13 @@ + 不,停止优化 + 是,继续优化 因此,我们建议使用 Web 浏览器编辑该区块。 因此,我们建议使用 Web 编辑器编辑该区块。 或者,您也可以取消区块分组,以使内容扁平化。 @@ -20,10 +22,6 @@ Language: zh_CN 嵌套深度超过 %d 层级的区块在移动端编辑器中可能无法正常渲染。 开始使用 是时候在 Jetpack 应用程序上继续您的 WordPress 之旅了。 - 优化图像可缩小图像尺寸,以便更快地上传。\n\n默认启用该选项,但是您可以随时在应用程序设置中进行更改。 - 继续优化图像? - 不,停止优化 - 是,继续优化 清除搜索 非常高 请提供您的安全密钥以继续。 @@ -100,6 +98,7 @@ Language: zh_CN 文章草稿 浏览次数、访客人数和点赞数 卡片可能会显示不同的内容,具体取决于您站点的情况 + 个性化“首页”选项卡 轻点以对您的“首页”选项卡进行个性化 对您的“首页”选项卡进行个性化 更改设置 diff --git a/WordPress/src/main/res/values-zh-rHK/strings.xml b/WordPress/src/main/res/values-zh-rHK/strings.xml index b5ddf5d51aad..f0a2c74c9ab6 100644 --- a/WordPress/src/main/res/values-zh-rHK/strings.xml +++ b/WordPress/src/main/res/values-zh-rHK/strings.xml @@ -1,11 +1,13 @@ + 否,關閉 + 是,保持開啟 因此,我們建議使用網頁瀏覽器來編輯區塊。 因此,我們建議使用網頁編輯器來編輯區塊。 或者,你可以取消區塊群組來簡化內容。 @@ -20,10 +22,8 @@ Language: zh_TW 行動編輯器可能無法正確顯示內嵌超過 %d 層的區塊。 我們開始吧 現在就在 Jetpack 應用程式上繼續你的 WordPress 旅程。 - 圖片最佳化會壓縮圖片,以加快上傳速度。\n\n系統預設啟用此選項,但你可以隨時在應用程式設定中變更。 - 要繼續最佳化圖片嗎? - 不,關閉設定 - 是,繼續使用 + 為加快上傳速度,圖片最佳化功能會壓縮圖片。\n\n系統預設啟用此選項,但你可以隨時在應用程式設定中變更。 + 是否要繼續將圖片最佳化? 清除搜尋 超高 請提供安全性金鑰以繼續使用。 @@ -101,6 +101,7 @@ Language: zh_TW 草稿文章 點閱數、訪客數與按讚數 卡片可根據您的網站活動顯示不同內容 + 打造個人版首頁分頁 點選以打造個人化「首頁」分頁 打造你的個人化「首頁」分頁 變更設定 diff --git a/WordPress/src/main/res/values-zh-rTW/strings.xml b/WordPress/src/main/res/values-zh-rTW/strings.xml index b5ddf5d51aad..f0a2c74c9ab6 100644 --- a/WordPress/src/main/res/values-zh-rTW/strings.xml +++ b/WordPress/src/main/res/values-zh-rTW/strings.xml @@ -1,11 +1,13 @@ + 否,關閉 + 是,保持開啟 因此,我們建議使用網頁瀏覽器來編輯區塊。 因此,我們建議使用網頁編輯器來編輯區塊。 或者,你可以取消區塊群組來簡化內容。 @@ -20,10 +22,8 @@ Language: zh_TW 行動編輯器可能無法正確顯示內嵌超過 %d 層的區塊。 我們開始吧 現在就在 Jetpack 應用程式上繼續你的 WordPress 旅程。 - 圖片最佳化會壓縮圖片,以加快上傳速度。\n\n系統預設啟用此選項,但你可以隨時在應用程式設定中變更。 - 要繼續最佳化圖片嗎? - 不,關閉設定 - 是,繼續使用 + 為加快上傳速度,圖片最佳化功能會壓縮圖片。\n\n系統預設啟用此選項,但你可以隨時在應用程式設定中變更。 + 是否要繼續將圖片最佳化? 清除搜尋 超高 請提供安全性金鑰以繼續使用。 @@ -101,6 +101,7 @@ Language: zh_TW 草稿文章 點閱數、訪客數與按讚數 卡片可根據您的網站活動顯示不同內容 + 打造個人版首頁分頁 點選以打造個人化「首頁」分頁 打造你的個人化「首頁」分頁 變更設定 diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 00931f1d1ff2..74586ccd9ea4 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -4803,21 +4803,42 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> - Scan Barcode - Camera permission is required to scan the barcode. - Grant Camera Permission - Camera permission is required in order to scan the barcode - You have permanently denied Camera permission. It is required in order to scan the barcode. Please enable it from the app settings - Grant - Cancel - Go to settings + Scan Barcode + Camera permission is required to scan the barcode. + Grant Camera Permission + Camera permission is required in order to scan the barcode + You have permanently denied Camera permission. It is required in order to scan the barcode. Please enable it from the app settings + Grant + Cancel + Go to settings Use a security key There was some trouble with the Security key login Please provide your security key to continue. + Alternatively, you can flatten the content by ungrouping the block. For this reason, we recommend editing the block using the web editor. For this reason, we recommend editing the block using your web browser. We cannot open media at the moment. Please try again later + + + Bloganuary is coming! + For the month of January, blogging prompts will come from Bloganuary - our community challenge to build a blogging habit for the new year. + @string/learn_more + Bloganuary + Join our month-long writing challenge + Receive a new prompt to inspire you each day. + Publish your response. + Read other bloggers’ responses to get inspiration and make new connections. + Bloganuary will use Daily Blogging Prompts to send you topics for the month of January. You have Blogging Prompts currently disabled. + Bloganuary will use Daily Blogging Prompts to send you topics for the month of January. + Turn on blogging prompts + Let’s go! + + + State of the Word 2023 + Check out WordPress co-founder Matt Mullenweg’s annual keynote to stay on top of what’s coming in 2024 and beyond. + Watch now + diff --git a/WordPress/src/test/java/org/wordpress/android/ui/bloganuary/BloganuaryNudgeAnalyticsTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/bloganuary/BloganuaryNudgeAnalyticsTrackerTest.kt new file mode 100644 index 000000000000..65d42c6117d5 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/bloganuary/BloganuaryNudgeAnalyticsTrackerTest.kt @@ -0,0 +1,101 @@ +package org.wordpress.android.ui.bloganuary + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.kotlin.verify +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.ui.bloganuary.BloganuaryNudgeAnalyticsTracker.BloganuaryNudgeCardMenuItem +import org.wordpress.android.ui.bloganuary.learnmore.BloganuaryNudgeLearnMoreOverlayAction +import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper + +@OptIn(ExperimentalCoroutinesApi::class) +class BloganuaryNudgeAnalyticsTrackerTest : BaseUnitTest() { + @Mock + lateinit var analyticsTracker: AnalyticsTrackerWrapper + + @Mock + lateinit var cardsTracker: CardsTracker + + lateinit var tracker: BloganuaryNudgeAnalyticsTracker + + @Before + fun setUp() { + tracker = BloganuaryNudgeAnalyticsTracker(analyticsTracker, cardsTracker) + } + + @Test + fun `WHEN trackMySiteCardLearnMoreTapped is called THEN analyticsTracker is called correctly`() { + listOf(true, false).forEach { isPromptsEnabled -> + tracker.trackMySiteCardLearnMoreTapped(isPromptsEnabled) + + verify(analyticsTracker).track( + Stat.BLOGANUARY_NUDGE_MY_SITE_CARD_LEARN_MORE_TAPPED, + mapOf("prompts_enabled" to isPromptsEnabled.toString()) + ) + } + } + + @Test + fun `WHEN trackMySiteCardMoreMenuTapped is called THEN cardsTracker is called correctly`() { + tracker.trackMySiteCardMoreMenuTapped() + + verify(cardsTracker).trackCardMoreMenuClicked( + CardsTracker.Type.BLOGANUARY_NUDGE.label + ) + } + + @Test + fun `WHEN trackMySiteCardMoreMenuItemTapped is called for hide_this THEN cardsTracker is called correctly`() { + tracker.trackMySiteCardMoreMenuItemTapped(BloganuaryNudgeCardMenuItem.HIDE_THIS) + + verify(cardsTracker).trackCardMoreMenuItemClicked( + CardsTracker.Type.BLOGANUARY_NUDGE.label, + "hide_this" + ) + } + + @Test + fun `WHEN trackLearnMoreOverlayShown is called THEN analyticsTracker is called correctly`() { + listOf(true, false).forEach { isPromptsEnabled -> + tracker.trackLearnMoreOverlayShown(isPromptsEnabled) + + verify(analyticsTracker).track( + Stat.BLOGANUARY_NUDGE_LEARN_MORE_MODAL_SHOWN, + mapOf("prompts_enabled" to isPromptsEnabled.toString()) + ) + } + } + + @Test + fun `WHEN trackLearnMoreOverlayDismissed is called THEN analyticsTracker is called correctly`() { + tracker.trackLearnMoreOverlayDismissed() + + verify(analyticsTracker).track( + Stat.BLOGANUARY_NUDGE_LEARN_MORE_MODAL_DISMISSED + ) + } + + @Test + fun `WHEN trackLearnMoreOverlayActionTapped for dismiss THEN analyticsTracker is called correctly`() { + tracker.trackLearnMoreOverlayActionTapped(BloganuaryNudgeLearnMoreOverlayAction.DISMISS) + + verify(analyticsTracker).track( + Stat.BLOGANUARY_NUDGE_LEARN_MORE_MODAL_ACTION_TAPPED, + mapOf("action" to "dismiss") + ) + } + + @Test + fun `WHEN trackLearnMoreOverlayActionTapped for turn_prompts_on THEN analyticsTracker is called correctly`() { + tracker.trackLearnMoreOverlayActionTapped(BloganuaryNudgeLearnMoreOverlayAction.TURN_ON_PROMPTS) + + verify(analyticsTracker).track( + Stat.BLOGANUARY_NUDGE_LEARN_MORE_MODAL_ACTION_TAPPED, + mapOf("action" to "turn_on_prompts") + ) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayViewModelTest.kt new file mode 100644 index 000000000000..a0396fedb437 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/bloganuary/learnmore/BloganuaryNudgeLearnMoreOverlayViewModelTest.kt @@ -0,0 +1,109 @@ +package org.wordpress.android.ui.bloganuary.learnmore + +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.verify +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.R +import org.wordpress.android.ui.bloganuary.BloganuaryNudgeAnalyticsTracker +import org.wordpress.android.ui.bloganuary.learnmore.BloganuaryNudgeLearnMoreOverlayViewModel.DismissEvent +import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper +import org.wordpress.android.ui.utils.UiString.UiStringRes + +@OptIn(ExperimentalCoroutinesApi::class) +class BloganuaryNudgeLearnMoreOverlayViewModelTest : BaseUnitTest() { + @Mock + lateinit var promptsSettingsHelper: BloggingPromptsSettingsHelper + + @Mock + lateinit var tracker: BloganuaryNudgeAnalyticsTracker + + lateinit var viewModel: BloganuaryNudgeLearnMoreOverlayViewModel + + @Before + fun setUp() { + viewModel = BloganuaryNudgeLearnMoreOverlayViewModel(promptsSettingsHelper, tracker) + } + + @Test + fun `getUiState should return correct UiState when prompts are enabled`() { + val uiState = viewModel.getUiState(isPromptsEnabled = true) + + assertThat(uiState).isEqualTo( + BloganuaryNudgeLearnMoreOverlayUiState( + noteText = UiStringRes(R.string.bloganuary_dashboard_nudge_overlay_note_prompts_enabled), + action = BloganuaryNudgeLearnMoreOverlayAction.DISMISS, + ) + ) + } + + @Test + fun `getUiState should return correct UiState when prompts are disabled`() { + val uiState = viewModel.getUiState(isPromptsEnabled = false) + + assertThat(uiState).isEqualTo( + BloganuaryNudgeLearnMoreOverlayUiState( + noteText = UiStringRes(R.string.bloganuary_dashboard_nudge_overlay_note_prompts_disabled), + action = BloganuaryNudgeLearnMoreOverlayAction.TURN_ON_PROMPTS, + ) + ) + } + + @Test + fun `onActionClick should dismiss dialog when action is DISMISS`() { + viewModel.onActionClick(BloganuaryNudgeLearnMoreOverlayAction.DISMISS) + + assertThat(viewModel.dismissDialog.value).isEqualTo(DismissEvent()) + } + + @Test + fun `onActionClick should turn on blogging prompts when action is TURN_ON_PROMPTS`() = test { + viewModel.onActionClick(BloganuaryNudgeLearnMoreOverlayAction.TURN_ON_PROMPTS) + + verify(promptsSettingsHelper).updatePromptsCardEnabledForCurrentSite(true) + } + + @Test + fun `onActionClick should dismiss dialog requesting refresh when action is TURN_ON_PROMPTS`() { + viewModel.onActionClick(BloganuaryNudgeLearnMoreOverlayAction.TURN_ON_PROMPTS) + + assertThat(viewModel.dismissDialog.value).isEqualTo(DismissEvent(refreshDashboard = true)) + } + + @Test + fun `onCloseClick should dismiss dialog`() { + viewModel.onCloseClick() + + assertThat(viewModel.dismissDialog.value).isEqualTo(DismissEvent()) + } + + // region Analytics + @Test + fun `onDialogShown should track analytics`() { + listOf(true, false).forEach { isPromptsEnabled -> + viewModel.onDialogShown(isPromptsEnabled) + + verify(tracker).trackLearnMoreOverlayShown(isPromptsEnabled) + } + } + + @Test + fun `onActionClick should track analytics`() { + BloganuaryNudgeLearnMoreOverlayAction.entries.forEach { + viewModel.onActionClick(it) + + verify(tracker).trackLearnMoreOverlayActionTapped(it) + } + } + + @Test + fun `onDialogDismissed should track analytics`() { + viewModel.onDialogDismissed() + + verify(tracker).trackLearnMoreOverlayDismissed() + } + // endregion +} 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 index 8c901817c0f7..942681c91225 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProviderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProviderTest.kt @@ -5,12 +5,12 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.mockito.Mock -import org.mockito.Mockito.mock import org.mockito.kotlin.any import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.ui.reader.services.post.ReaderPostLogic import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import kotlin.test.assertEquals @@ -23,9 +23,6 @@ class BloggingPromptsPostTagProviderTest : BaseUnitTest() { @Before fun setUp() { - whenever(readerUtilsWrapper.getTagFromTagName(BLOGGING_PROMPT_ID_TAG, ReaderTagType.FOLLOWED)) - .thenReturn(BLOGGING_PROMPT_ID_READER_TAG) - tagProvider = BloggingPromptsPostTagProvider(readerUtilsWrapper) } @@ -50,14 +47,18 @@ class BloggingPromptsPostTagProviderTest : BaseUnitTest() { @Test fun `Should return the expected ReaderTag when promptIdSearchReaderTag is called`() { whenever(readerUtilsWrapper.getTagFromTagUrl(any())).thenReturn(BLOGGING_PROMPT_ID_TAG) - + val expected = ReaderTag( + BLOGGING_PROMPT_ID_TAG, + BLOGGING_PROMPT_ID_TAG, + BLOGGING_PROMPT_ID_TAG, + ReaderPostLogic.formatFullEndpointForTag(BLOGGING_PROMPT_ID_TAG), + ReaderTagType.FOLLOWED, + ) val actual = tagProvider.promptIdSearchReaderTag("valid-url") - - assertEquals(BLOGGING_PROMPT_ID_READER_TAG, actual) + assertEquals(expected, actual) } companion object { private const val BLOGGING_PROMPT_ID_TAG = "dailyprompt-1234" - private val BLOGGING_PROMPT_ID_READER_TAG = mock() } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsSettingsHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsSettingsHelperTest.kt index d30deeb44695..2b9ca4b6e940 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsSettingsHelperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsSettingsHelperTest.kt @@ -98,6 +98,22 @@ class BloggingPromptsSettingsHelperTest : BaseUnitTest() { verify(bloggingRemindersStore).updateBloggingReminders(argThat { isPromptsCardEnabled == expectedState }) } + @Test + fun `when updatePromptsCardEnabledForCurrentSite, then updates the store model`() = test { + val expectedState = true + val model = createRemindersModel(isPromptsCardEnabled = false) + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(createSiteModel()) + whenever(bloggingRemindersStore.bloggingRemindersModel(any())).doAnswer { + flowOf(model) + } + + helper.updatePromptsCardEnabledForCurrentSite(isEnabled = expectedState) + + verify(bloggingRemindersStore).updateBloggingReminders( + argThat { siteId == 123 && isPromptsCardEnabled == expectedState } + ) + } + @Test fun `given prompts FF is off and site is wpcom site, when isPromptsFeatureAvailable, then returns false`() { whenever(bloggingPromptsFeature.isEnabled()).thenReturn(false) @@ -138,7 +154,7 @@ class BloggingPromptsSettingsHelperTest : BaseUnitTest() { } @Test - fun `given site is not selected, when isPromptsFeatureActive, then returns false`() = test { + fun `given site is not selected, when shouldShowPromptsFeature, then returns false`() = test { whenever(selectedSiteRepository.getSelectedSite()).thenReturn(null) val result = helper.shouldShowPromptsFeature() @@ -147,7 +163,7 @@ class BloggingPromptsSettingsHelperTest : BaseUnitTest() { } @Test - fun `given prompts feature not available, when isPromptsFeatureActive, then returns false`() = test { + fun `given prompts feature not available, when shouldShowPromptsFeature, then returns false`() = test { whenever(bloggingPromptsFeature.isEnabled()).thenReturn(false) whenever(selectedSiteRepository.getSelectedSite()).thenReturn(createSiteModel()) @@ -157,7 +173,7 @@ class BloggingPromptsSettingsHelperTest : BaseUnitTest() { } @Test - fun `given prompts setting off, when isPromptsFeatureActive, then returns false`() = test { + fun `given prompts setting off, when shouldShowPromptsFeature, then returns false`() = test { // prompts feature is available whenever(bloggingPromptsFeature.isEnabled()).thenReturn(true) whenever(selectedSiteRepository.getSelectedSite()).thenReturn(createSiteModel()) @@ -172,9 +188,8 @@ class BloggingPromptsSettingsHelperTest : BaseUnitTest() { assertThat(result).isFalse } - @Suppress("MaxLineLength") @Test - fun `given prompts setting on and skipped for today, when isPromptsFeatureActive, then returns false`() = + fun `given prompts setting on and skipped for today, when shouldShowPromptsFeature, then returns false`() = test { // prompts feature is available whenever(bloggingPromptsFeature.isEnabled()).thenReturn(true) @@ -193,9 +208,8 @@ class BloggingPromptsSettingsHelperTest : BaseUnitTest() { } - @Suppress("MaxLineLength") @Test - fun `given prompts setting on and not skipped for today, when isPromptsFeatureActive, then returns false`() = + fun `given prompts setting on and not skipped for today, when shouldShowPromptsFeature, then returns true`() = test { // prompts feature is available whenever(bloggingPromptsFeature.isEnabled()).thenReturn(true) @@ -251,7 +265,40 @@ class BloggingPromptsSettingsHelperTest : BaseUnitTest() { val result = helper.shouldShowPromptsSetting() - assertThat(result).isTrue() + assertThat(result).isTrue + } + + @Test + fun `given site is not selected, when isPromptsSettingEnabled, then returns false`() = test { + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(null) + + val result = helper.isPromptsSettingEnabled() + + assertThat(result).isFalse + } + + @Test + fun `given prompts card setting disabled, when isPromptsSettingEnabled, then returns false`() = test { + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(createSiteModel()) + whenever(bloggingRemindersStore.bloggingRemindersModel(any())).doAnswer { + flowOf(createRemindersModel(isPromptsCardEnabled = false)) + } + + val result = helper.isPromptsSettingEnabled() + + assertThat(result).isFalse + } + + @Test + fun `given prompts card setting enabled, when isPromptsSettingEnabled, then returns true`() = test { + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(createSiteModel()) + whenever(bloggingRemindersStore.bloggingRemindersModel(any())).doAnswer { + flowOf(createRemindersModel(isPromptsCardEnabled = true)) + } + + val result = helper.isPromptsSettingEnabled() + + assertThat(result).isTrue } companion object { 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 d7d4b77ff827..54beb07c7607 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 @@ -51,6 +51,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.ErrorCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.JetpackFeatureCard 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.MySiteCardAndItem.Card.WpSotw2023NudgeCardModel import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.InfoItem import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.ListItem import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.SingleActionCard @@ -76,6 +77,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.activity.ActivityLogCardViewModelSlice +import org.wordpress.android.ui.mysite.cards.dashboard.bloganuary.BloganuaryNudgeCardViewModelSlice import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptCardViewModelSlice import org.wordpress.android.ui.mysite.cards.dashboard.domaintransfer.DomainTransferCardViewModel import org.wordpress.android.ui.mysite.cards.dashboard.pages.PagesCardViewModelSlice @@ -96,6 +98,7 @@ import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository.QuickStartCategory import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardBuilder import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardViewModelSlice +import org.wordpress.android.ui.mysite.cards.sotw2023.WpSotw2023NudgeCardViewModelSlice import org.wordpress.android.ui.mysite.items.infoitem.MySiteInfoItemBuilder import org.wordpress.android.ui.mysite.items.listitem.ListItemAction import org.wordpress.android.ui.mysite.items.listitem.SiteItemsBuilder @@ -277,6 +280,12 @@ class MySiteViewModelTest : BaseUnitTest() { @Mock lateinit var quickLinksItemViewModelSlice: QuickLinksItemViewModelSlice + @Mock + lateinit var bloganuaryNudgeViewModelSlice: BloganuaryNudgeCardViewModelSlice + + @Mock + lateinit var wpSotw2023NudgeCardViewModelSlice: WpSotw2023NudgeCardViewModelSlice + private lateinit var viewModel: MySiteViewModel private lateinit var uiModels: MutableList private lateinit var snackbars: MutableList @@ -412,9 +421,11 @@ class MySiteViewModelTest : BaseUnitTest() { whenever(activityLogCardViewModelSlice.getActivityLogCardBuilderParams(anyOrNull())).thenReturn(mock()) whenever(personalizeCardViewModelSlice.getBuilderParams()).thenReturn(mock()) whenever(personalizeCardBuilder.build(any())).thenReturn(mock()) + whenever(bloganuaryNudgeViewModelSlice.getBuilderParams()).thenReturn(mock()) whenever(bloggingPromptCardViewModelSlice.getBuilderParams(anyOrNull())).thenReturn(mock()) whenever(quickLinksItemViewModelSlice.uiState).thenReturn(mock()) whenever(quickStartRepository.quickStartMenuStep).thenReturn(mock()) + whenever(wpSotw2023NudgeCardViewModelSlice.buildCard()).thenReturn(null) viewModel = MySiteViewModel( testDispatcher(), @@ -467,7 +478,9 @@ class MySiteViewModelTest : BaseUnitTest() { bloggingPromptCardViewModelSlice, noCardsMessageViewModelSlice, siteInfoHeaderCardViewModelSlice, - quickLinksItemViewModelSlice + quickLinksItemViewModelSlice, + bloganuaryNudgeViewModelSlice, + wpSotw2023NudgeCardViewModelSlice, ) uiModels = mutableListOf() snackbars = mutableListOf() @@ -1424,6 +1437,24 @@ class MySiteViewModelTest : BaseUnitTest() { assertThat(viewModel.onShowJetpackIndividualPluginOverlay.value?.peekContent()).isNull() } + @Test + fun `when sotw card is not null then it is shown in the site menu`() { + whenever(wpSotw2023NudgeCardViewModelSlice.buildCard()).thenReturn(mock()) + + initSelectedSite() + + assertThat(getSiteMenuTabLastItems().filterIsInstance(WpSotw2023NudgeCardModel::class.java)).isNotEmpty + } + + @Test + fun `when sotw card is null then it is not shown in the site menu`() { + whenever(wpSotw2023NudgeCardViewModelSlice.buildCard()).thenReturn(null) + + initSelectedSite() + + assertThat(getSiteMenuTabLastItems().filterIsInstance(WpSotw2023NudgeCardModel::class.java)).isEmpty() + } + private fun findDomainRegistrationCard() = getLastItems().find { it is DomainRegistrationCard } as DomainRegistrationCard? 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 d2d3e6d7f605..41043b3ee07f 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 @@ -18,6 +18,7 @@ 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.BlazeCardBuilderParams +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BloganuaryNudgeCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BloggingPromptCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardPlansBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardsBuilderParams @@ -131,6 +132,12 @@ class CardsBuilderTest { onErrorRetryClick = mock(), todaysStatsCardBuilderParams = TodaysStatsCardBuilderParams(mock(), mock(), mock(), mock()), postCardBuilderParams = PostCardBuilderParams(mock(), mock(), mock()), + bloganuaryNudgeCardBuilderParams = BloganuaryNudgeCardBuilderParams( + false, + mock(), + mock(), + mock(), + ), bloggingPromptCardBuilderParams = BloggingPromptCardBuilderParams( mock(), 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 7ddc25389bba..e68c2f6e75a7 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 @@ -21,6 +21,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.ErrorCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.PostCard.PostCardWithPostItems import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.PagesCard.PagesCardWithData import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BlazeCard.PromoteWithBlazeCard +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.BloganuaryNudgeCardModel import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.TodaysStatsCard.TodaysStatsCardWithData import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardPlansBuilderParams @@ -29,9 +30,11 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardC import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.PostCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.TodaysStatsCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BlazeCardBuilderParams.PromoteWithBlazeCardBuilderParams +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BloganuaryNudgeCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DomainTransferCardBuilderParams import org.wordpress.android.ui.mysite.cards.blaze.BlazeCardBuilder import org.wordpress.android.ui.mysite.cards.dashboard.activity.ActivityCardBuilder +import org.wordpress.android.ui.mysite.cards.dashboard.bloganuary.BloganuaryNudgeCardBuilder 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 @@ -68,6 +71,9 @@ class CardsBuilderTest : BaseUnitTest() { @Mock lateinit var mDomainTransferCardBuilder: DomainTransferCardBuilder + @Mock + lateinit var bloganuaryCardBuilder: BloganuaryNudgeCardBuilder + private lateinit var cardsBuilder: CardsBuilder @Before @@ -75,6 +81,7 @@ class CardsBuilderTest : BaseUnitTest() { cardsBuilder = CardsBuilder( todaysStatsCardBuilder, postCardBuilder, + bloganuaryCardBuilder, bloggingPromptCardsBuilder, mDomainTransferCardBuilder, blazeCardBuilder, @@ -229,6 +236,21 @@ class CardsBuilderTest : BaseUnitTest() { assertThat(cards.findActivityCard()).isNotNull } + /* BLOGANUARY CARD */ + @Test + fun `when is eligible for bloganuary nudge card, then bloganuary nudge card is built`() { + val cards = buildDashboardCards(isEligibleForBloganuaryNudge = true) + + assertThat(cards.findBloganuaryNudgeCard()).isNotNull + } + + @Test + fun `when is not eligible for bloganuary nudge card, then bloganuary nudge card is not built`() { + val cards = buildDashboardCards(isEligibleForBloganuaryNudge = false) + + assertThat(cards.findBloganuaryNudgeCard()).isNull() + } + private fun List.findTodaysStatsCard() = this.find { it is TodaysStatsCardWithData } as? TodaysStatsCardWithData @@ -250,6 +272,9 @@ class CardsBuilderTest : BaseUnitTest() { private fun List.findActivityCard() = this.find { it is ActivityCardWithItems } as? ActivityCardWithItems + private fun List.findBloganuaryNudgeCard() = + this.find { it is BloganuaryNudgeCardModel } as? BloganuaryNudgeCardModel + private fun List.findErrorCard() = this.find { it is ErrorCard } as? ErrorCard private val todaysStatsCard = mock() @@ -264,6 +289,8 @@ class CardsBuilderTest : BaseUnitTest() { private val activityCard = mock() + private val bloganuaryNudgeCard = mock() + private fun createPostCards() = listOf( PostCardWithPostItems( postCardType = DRAFT, @@ -282,8 +309,9 @@ class CardsBuilderTest : BaseUnitTest() { isEligibleForDomainTransferCard: Boolean = false, isEligibleForBlaze: Boolean = false, isEligibleForPlansCard: Boolean = false, + isEligibleForBloganuaryNudge: Boolean = false, hasPagesCard: Boolean = false, - hasActivityCard: Boolean = false + hasActivityCard: Boolean = false, ): List { doAnswer { if (hasTodaysStats) todaysStatsCard else null }.whenever(todaysStatsCardBuilder).build(any()) doAnswer { if (hasPostsForPostCard) createPostCards() else emptyList() }.whenever(postCardBuilder) @@ -293,6 +321,8 @@ class CardsBuilderTest : BaseUnitTest() { .build(any()) doAnswer { if (isEligibleForPlansCard) dashboardPlansCard else null }.whenever(dashboardPlansCardBuilder) .build(any()) + doAnswer { if (isEligibleForBloganuaryNudge) bloganuaryNudgeCard else null }.whenever(bloganuaryCardBuilder) + .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( @@ -301,6 +331,12 @@ class CardsBuilderTest : BaseUnitTest() { onErrorRetryClick = { }, todaysStatsCardBuilderParams = TodaysStatsCardBuilderParams(mock(), mock(), mock(), mock()), postCardBuilderParams = PostCardBuilderParams(mock(), mock(), mock()), + bloganuaryNudgeCardBuilderParams = BloganuaryNudgeCardBuilderParams( + isEligibleForBloganuaryNudge, + mock(), + mock(), + mock(), + ), bloggingPromptCardBuilderParams = BloggingPromptCardBuilderParams( mock(), mock(), mock(), mock(), mock(), mock(), mock() ), diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardBuilderTest.kt new file mode 100644 index 000000000000..d0b2d0af5341 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardBuilderTest.kt @@ -0,0 +1,47 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.bloganuary + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BloganuaryNudgeCardBuilderParams +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.R + +class BloganuaryNudgeCardBuilderTest { + @Test + fun `GIVEN not eligible, WHEN build is called, THEN return null`() { + val params = BloganuaryNudgeCardBuilderParams( + isEligible = false, + onLearnMoreClick = {}, + onMoreMenuClick = {}, + onHideMenuItemClick = {}, + ) + val cardModel = BloganuaryNudgeCardBuilder().build(params) + assertThat(cardModel).isNull() + } + + @Test + fun `GIVEN eligible, WHEN build is called, THEN return correct model`() { + var currentAction = "" + + val params = BloganuaryNudgeCardBuilderParams( + isEligible = true, + onLearnMoreClick = { currentAction = "onLearnMoreClick" }, + onMoreMenuClick = { currentAction = "onMoreMenuClick" }, + onHideMenuItemClick = { currentAction = "onHideMenuItemClick" }, + ) + val cardModel = BloganuaryNudgeCardBuilder().build(params) + + assertThat(cardModel!!).isNotNull + assertThat(cardModel.title).isEqualTo(UiString.UiStringRes(R.string.bloganuary_dashboard_nudge_title)) + assertThat(cardModel.text).isEqualTo(UiString.UiStringRes(R.string.bloganuary_dashboard_nudge_text)) + + // check if the callbacks are hooked correctly + assertThat(currentAction).isEmpty() + cardModel.onLearnMoreClick.click() + assertThat(currentAction).isEqualTo("onLearnMoreClick") + cardModel.onMoreMenuClick.click() + assertThat(currentAction).isEqualTo("onMoreMenuClick") + cardModel.onHideMenuItemClick.click() + assertThat(currentAction).isEqualTo("onHideMenuItemClick") + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardViewModelSliceTest.kt new file mode 100644 index 000000000000..c4965f663498 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/bloganuary/BloganuaryNudgeCardViewModelSliceTest.kt @@ -0,0 +1,222 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.bloganuary + +import android.icu.util.Calendar +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.Mockito +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +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.ui.bloganuary.BloganuaryNudgeAnalyticsTracker +import org.wordpress.android.ui.bloganuary.BloganuaryNudgeAnalyticsTracker.BloganuaryNudgeCardMenuItem +import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper +import org.wordpress.android.ui.mysite.SelectedSiteRepository +import org.wordpress.android.ui.mysite.SiteNavigationAction +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.util.DateTimeUtilsWrapper +import org.wordpress.android.util.config.BloganuaryNudgeFeatureConfig + +@OptIn(ExperimentalCoroutinesApi::class) +class BloganuaryNudgeCardViewModelSliceTest : BaseUnitTest() { + @Mock + lateinit var bloganuaryNudgeFeatureConfig: BloganuaryNudgeFeatureConfig + + @Mock + lateinit var bloggingPromptsSettingsHelper: BloggingPromptsSettingsHelper + + @Mock + lateinit var selectedSiteRepository: SelectedSiteRepository + + @Mock + lateinit var appPrefsWrapper: AppPrefsWrapper + + @Mock + lateinit var tracker: BloganuaryNudgeAnalyticsTracker + + @Mock + lateinit var dateTimeUtilsWrapper: DateTimeUtilsWrapper + + lateinit var viewModel: BloganuaryNudgeCardViewModelSlice + + @Before + fun setUp() { + viewModel = BloganuaryNudgeCardViewModelSlice( + bloganuaryNudgeFeatureConfig, + bloggingPromptsSettingsHelper, + selectedSiteRepository, + appPrefsWrapper, + tracker, + dateTimeUtilsWrapper, + ) + viewModel.initialize(testScope()) + } + + @Test + fun `GIVEN FF disabled, WHEN getting builder params, THEN not eligible`() { + whenever(bloganuaryNudgeFeatureConfig.isEnabled()).thenReturn(false) + + val params = viewModel.getBuilderParams() + + assertThat(params.isEligible).isFalse + } + + @Test + fun `GIVEN not December, WHEN getting builder params, THEN not eligible`() { + // need to use it to make sure that test will fail if other month meets the requirement incorrectly + val lenient = Mockito.lenient() + + whenever(bloganuaryNudgeFeatureConfig.isEnabled()).thenReturn(true) + lenient.`when`(bloggingPromptsSettingsHelper.isPromptsFeatureAvailable()).thenReturn(true) + lenient.`when`(selectedSiteRepository.getSelectedSite()).thenReturn(mockSiteModel) + lenient.`when`(appPrefsWrapper.getShouldHideBloganuaryNudgeCard(SITE_ID)).thenReturn(false) + + // Test all months except December + (Calendar.JANUARY..Calendar.NOVEMBER).forEach { month -> + mockCalendarMonth(month) + + val params = viewModel.getBuilderParams() + + assertThat(params.isEligible).isFalse + Mockito.reset(dateTimeUtilsWrapper) + } + } + + @Test + fun `GIVEN prompts not available, WHEN getting builder params, THEN not eligible`() { + whenever(bloganuaryNudgeFeatureConfig.isEnabled()).thenReturn(true) + mockCalendarMonth(Calendar.DECEMBER) + whenever(bloggingPromptsSettingsHelper.isPromptsFeatureAvailable()).thenReturn(false) + + val params = viewModel.getBuilderParams() + + assertThat(params.isEligible).isFalse + } + + @Test + fun `GIVEN no selected site, WHEN getting builder params, THEN not eligible`() { + whenever(bloganuaryNudgeFeatureConfig.isEnabled()).thenReturn(true) + mockCalendarMonth(Calendar.DECEMBER) + whenever(bloggingPromptsSettingsHelper.isPromptsFeatureAvailable()).thenReturn(true) + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(null) + + val params = viewModel.getBuilderParams() + + assertThat(params.isEligible).isFalse + } + + @Test + fun `GIVEN card was hidden by user, WHEN getting builder params, THEN not eligible`() { + whenever(bloganuaryNudgeFeatureConfig.isEnabled()).thenReturn(true) + mockCalendarMonth(Calendar.DECEMBER) + whenever(bloggingPromptsSettingsHelper.isPromptsFeatureAvailable()).thenReturn(true) + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(mockSiteModel) + whenever(appPrefsWrapper.getShouldHideBloganuaryNudgeCard(SITE_ID)).thenReturn(true) + + val params = viewModel.getBuilderParams() + + assertThat(params.isEligible).isFalse + } + + @Test + fun `GIVEN requirements met, WHEN getting builder params, THEN eligible`() { + mockEligibleRequirements() + + val params = viewModel.getBuilderParams() + + assertThat(params.isEligible).isTrue + } + + @Test + fun `GIVEN builder params, WHEN calling onLearnMoreClick, THEN navigate to overlay`() = test { + val isPromptsEnabled = true + whenever(bloggingPromptsSettingsHelper.isPromptsSettingEnabled()).thenReturn(isPromptsEnabled) + + mockEligibleRequirements() + + val params = viewModel.getBuilderParams() + params.onLearnMoreClick.invoke() + + advanceUntilIdle() + assertThat(viewModel.onNavigation.value?.peekContent()).isEqualTo( + SiteNavigationAction.OpenBloganuaryNudgeOverlay(isPromptsEnabled) + ) + } + + @Test + fun `GIVEN builder params, WHEN calling onHideMenuItemClick, THEN hide card in AppPrefs and refresh`() = test { + mockEligibleRequirements() + + val params = viewModel.getBuilderParams() + params.onHideMenuItemClick.invoke() + + advanceUntilIdle() + verify(appPrefsWrapper).setShouldHideBloganuaryNudgeCard(SITE_ID, true) + assertThat(viewModel.refresh.value?.peekContent()).isTrue + } + + // region Analytics + @Test + fun `GIVEN builder params, WHEN calling onLearnMoreClick, THEN track analytics`() = test { + val isPromptsEnabled = true + whenever(bloggingPromptsSettingsHelper.isPromptsSettingEnabled()).thenReturn(isPromptsEnabled) + + mockEligibleRequirements() + + val params = viewModel.getBuilderParams() + params.onLearnMoreClick.invoke() + + advanceUntilIdle() + verify(tracker).trackMySiteCardLearnMoreTapped(isPromptsEnabled) + } + + @Test + fun `GIVEN builder params, WHEN calling onMoreMenuClick, THEN track analytics`() = test { + mockEligibleRequirements() + + val params = viewModel.getBuilderParams() + params.onMoreMenuClick.invoke() + + advanceUntilIdle() + verify(tracker).trackMySiteCardMoreMenuTapped() + } + + @Test + fun `GIVEN builder params, WHEN calling onHideMenuItemClick, THEN track analytics`() = test { + mockEligibleRequirements() + + val params = viewModel.getBuilderParams() + params.onHideMenuItemClick.invoke() + + advanceUntilIdle() + verify(tracker).trackMySiteCardMoreMenuItemTapped(BloganuaryNudgeCardMenuItem.HIDE_THIS) + } + // endregion + + private fun mockCalendarMonth(month: Int) { + val mockCalendar: Calendar = mock { + on { get(Calendar.MONTH) } doReturn month + } + whenever(dateTimeUtilsWrapper.getCalendarInstance()).thenReturn(mockCalendar) + } + + private fun mockEligibleRequirements() { + whenever(bloganuaryNudgeFeatureConfig.isEnabled()).thenReturn(true) + mockCalendarMonth(Calendar.DECEMBER) + whenever(bloggingPromptsSettingsHelper.isPromptsFeatureAvailable()).thenReturn(true) + whenever(selectedSiteRepository.getSelectedSite()).thenReturn(mockSiteModel) + whenever(appPrefsWrapper.getShouldHideBloganuaryNudgeCard(SITE_ID)).thenReturn(false) + } + + companion object { + private const val SITE_ID = 1L + private val mockSiteModel: SiteModel = mock { + on { siteId } doReturn SITE_ID + } + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt index dc057216dcdd..21880f63df13 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt @@ -87,7 +87,7 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() { assertThat(navigationActions) .containsOnly( - SiteNavigationAction.OpenTodaysStatsGetMoreViewsExternalUrl( + SiteNavigationAction.OpenExternalUrl( TodaysStatsCardBuilder.URL_GET_MORE_VIEWS_AND_TRAFFIC ) ) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTrackerTest.kt new file mode 100644 index 000000000000..64f442fa6cbc --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardAnalyticsTrackerTest.kt @@ -0,0 +1,62 @@ +package org.wordpress.android.ui.mysite.cards.sotw2023 + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper + +@OptIn(ExperimentalCoroutinesApi::class) +class WpSotw2023NudgeCardAnalyticsTrackerTest : BaseUnitTest() { + @Mock + lateinit var analyticsTracker: AnalyticsTrackerWrapper + + lateinit var tracker: WpSotw2023NudgeCardAnalyticsTracker + + @Before + fun setUp() { + tracker = WpSotw2023NudgeCardAnalyticsTracker(analyticsTracker) + } + + @Test + fun `WHEN card is shown THEN trackShown is called`() { + tracker.trackShown() + + verify(analyticsTracker).track(Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN) + } + + @Test + fun `WHEN card is shown multiple times without resetting THEN trackShown is called once`() { + tracker.trackShown() + tracker.trackShown() + + verify(analyticsTracker, times(1)).track(Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN) + } + + @Test + fun `WHEN card is shown multiple times with resetting THEN trackShown is called multiple times`() { + tracker.trackShown() + tracker.resetShown() + tracker.trackShown() + + verify(analyticsTracker, times(2)).track(Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN) + } + + @Test + fun `WHEN card CTA is tapped THEN trackCtaTapped is called`() { + tracker.trackCtaTapped() + + verify(analyticsTracker).track(Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_CTA_TAPPED) + } + + @Test + fun `WHEN card hide is tapped THEN trackHideTapped is called`() { + tracker.trackHideTapped() + + verify(analyticsTracker).track(Stat.SOTW_2023_NUDGE_POST_EVENT_CARD_HIDE_TAPPED) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSliceTest.kt new file mode 100644 index 000000000000..19cb2f2f4330 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/sotw2023/WpSotw2023NudgeCardViewModelSliceTest.kt @@ -0,0 +1,166 @@ +package org.wordpress.android.ui.mysite.cards.sotw2023 + +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.Mockito +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenExternalUrl +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.util.DateTimeUtilsWrapper +import org.wordpress.android.util.LocaleManagerWrapper +import org.wordpress.android.util.config.WpSotw2023NudgeFeatureConfig +import java.time.Instant + +@OptIn(ExperimentalCoroutinesApi::class) +class WpSotw2023NudgeCardViewModelSliceTest : BaseUnitTest() { + @Mock + lateinit var featureConfig: WpSotw2023NudgeFeatureConfig + + @Mock + lateinit var appPrefsWrapper: AppPrefsWrapper + + @Mock + lateinit var dateTimeUtilsWrapper: DateTimeUtilsWrapper + + @Mock + lateinit var localeManagerWrapper: LocaleManagerWrapper + + @Mock + lateinit var tracker: WpSotw2023NudgeCardAnalyticsTracker + + private lateinit var viewModelSlice: WpSotw2023NudgeCardViewModelSlice + + @Before + fun setUp() { + viewModelSlice = WpSotw2023NudgeCardViewModelSlice( + featureConfig, + appPrefsWrapper, + dateTimeUtilsWrapper, + localeManagerWrapper, + tracker, + ) + viewModelSlice.initialize(testScope()) + } + + @Test + fun `WHEN feature is disabled THEN buildCard returns null `() { + mockCardRequisites(isFeatureEnabled = false) + + val card = viewModelSlice.buildCard() + + assertThat(card).isNull() + } + + @Test + fun `WHEN card is hidden in app prefs THEN buildCard returns null`() { + mockCardRequisites(isCardHidden = true) + + val card = viewModelSlice.buildCard() + + assertThat(card).isNull() + } + + @Test + fun `WHEN date is before event THEN buildCard returns null`() { + mockCardRequisites(isDateAfterEvent = false) + + val card = viewModelSlice.buildCard() + + assertThat(card).isNull() + } + + @Test + fun `WHEN language is not english THEN buildCard returns null`() { + mockCardRequisites(isLanguageEnglish = false) + + val card = viewModelSlice.buildCard() + + assertThat(card).isNull() + } + + @Test + fun `WHEN requisites are met THEN buildCard returns card `() { + mockCardRequisites() + + val card = viewModelSlice.buildCard() + + assertThat(card).isNotNull + } + + @Test + fun `WHEN card onCtaClick is clicked THEN navigate to URL`() { + mockCardRequisites() + + val card = viewModelSlice.buildCard()!! + card.onCtaClick.click() + + assertThat(viewModelSlice.onNavigation.value?.peekContent()).isEqualTo(OpenExternalUrl(EXPECTED_URL)) + } + + @Test + fun `WHEN card onHideMenuItemClick is clicked THEN hide card in app prefs and refresh`() { + mockCardRequisites() + + val card = viewModelSlice.buildCard()!! + card.onHideMenuItemClick.click() + + verify(appPrefsWrapper).setShouldHideSotw2023NudgeCard(true) + assertThat(viewModelSlice.refresh.value?.peekContent()).isTrue + } + + // region Analytics + @Test + fun `WHEN card is shown THEN analytics is tracked`() { + mockCardRequisites() + + viewModelSlice.trackShown() + + verify(tracker).trackShown() + } + + @Test + fun `WHEN card onCtaClick is clicked THEN analytics is tracked`() { + mockCardRequisites() + + val card = viewModelSlice.buildCard()!! + card.onCtaClick.click() + verify(tracker).trackCtaTapped() + } + + @Test + fun `WHEN card onHideMenuItemClick is clicked THEN analytics is tracked`() { + mockCardRequisites() + + val card = viewModelSlice.buildCard()!! + card.onHideMenuItemClick.click() + + verify(tracker).trackHideTapped() + } + // endregion Analytics + + private fun mockCardRequisites( + isFeatureEnabled: Boolean = true, + isCardHidden: Boolean = false, + isDateAfterEvent: Boolean = true, + isLanguageEnglish: Boolean = true + ) { + with(Mockito.lenient()) { + whenever(featureConfig.isEnabled()).thenReturn(isFeatureEnabled) + whenever(appPrefsWrapper.getShouldHideSotw2023NudgeCard()).thenReturn(isCardHidden) + val date = if (isDateAfterEvent) "2023-12-12T00:00:01Z" else "2021-12-11T00:00:00Z" + whenever(dateTimeUtilsWrapper.getInstantNow()).thenReturn(Instant.parse(date)) + val language = if (isLanguageEnglish) "en_US" else "fr_FR" + whenever(localeManagerWrapper.getLanguage()).thenReturn(language) + } + } + + companion object { + private const val EXPECTED_URL = "https://wordpress.org/state-of-the-word/" + + "?utm_source=mobile&utm_medium=appnudge&utm_campaign=sotw2023" + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthViewModelTest.kt index 6088a7c684c3..7ef417a884f2 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthViewModelTest.kt @@ -28,10 +28,6 @@ import org.wordpress.android.fluxc.store.qrcodeauth.QRCodeAuthStore import org.wordpress.android.fluxc.store.qrcodeauth.QRCodeAuthStore.QRCodeAuthAuthenticateResult import org.wordpress.android.fluxc.store.qrcodeauth.QRCodeAuthStore.QRCodeAuthResult import org.wordpress.android.fluxc.store.qrcodeauth.QRCodeAuthStore.QRCodeAuthValidateResult -import org.wordpress.android.ui.barcodescanner.BarcodeScanningTracker -import org.wordpress.android.ui.barcodescanner.CodeScannerStatus -import org.wordpress.android.ui.barcodescanner.CodeScanningErrorType -import org.wordpress.android.ui.barcodescanner.ScanningSource import org.wordpress.android.ui.posts.BasicDialogViewModel.DialogInteraction import org.wordpress.android.ui.qrcodeauth.QRCodeAuthUiState.Content.Done import org.wordpress.android.ui.qrcodeauth.QRCodeAuthUiState.Content.Validated @@ -84,8 +80,6 @@ class QRCodeAuthViewModelTest : BaseUnitTest() { @Mock lateinit var analyticsTrackerWrapper: AnalyticsTrackerWrapper - @Mock - lateinit var barcodeScanningTracker: BarcodeScanningTracker private val uiStateMapper = QRCodeAuthUiStateMapper() private val validQueryParams = mapOf(DATA_KEY to DATA, TOKEN_KEY to TOKEN) @@ -95,8 +89,6 @@ class QRCodeAuthViewModelTest : BaseUnitTest() { private val errorTrackingMapAuthFailed = mutableMapOf("error" to "authentication_failed", "origin" to "menu") private val errorTrackingMapExpiredToken = mutableMapOf("error" to "expired_token", "origin" to "menu") - private val failureStatus = CodeScannerStatus.Failure("Failure", CodeScanningErrorType.Unknown) - @Before fun setUp() { viewModel = QRCodeAuthViewModel( @@ -104,8 +96,7 @@ class QRCodeAuthViewModelTest : BaseUnitTest() { uiStateMapper, networkUtilsWrapper, validator, - analyticsTrackerWrapper, - barcodeScanningTracker + analyticsTrackerWrapper ) whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) @@ -207,7 +198,6 @@ class QRCodeAuthViewModelTest : BaseUnitTest() { viewModel.start() viewModel.onScanSuccess(SCANNED_VALUE) - verify(barcodeScanningTracker).trackSuccess(ScanningSource.QRCODE_LOGIN) verify(analyticsTrackerWrapper).track(eq(QRLOGIN_VERIFY_FAILED), eq(errorTrackingMapInvalidData)) } } @@ -520,22 +510,12 @@ class QRCodeAuthViewModelTest : BaseUnitTest() { fun `when scan fails, then finish activity event is raised`() { val actionEvents = mutableListOf() testWithData(actionEvents = actionEvents) { - viewModel.onScanFailure(failureStatus) + viewModel.onScanFailure() assertThat(actionEvents.last()).isInstanceOf(QRCodeAuthActionEvent.FinishActivity::class.java) } } - @Test - fun `when scan fails, then scan failed is tracked`() { - val actionEvents = mutableListOf() - testWithData(actionEvents = actionEvents) { - viewModel.onScanFailure(failureStatus) - - verify(barcodeScanningTracker).trackScanFailure(ScanningSource.QRCODE_LOGIN, failureStatus.type) - } - } - @Test fun `given valid scan, when no network connection, then no internet error is shown`() { val uiStates = mutableListOf() 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 c719ff4683b0..b30f1988c9a2 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 @@ -364,6 +364,19 @@ class SiteCreationMainVMTest : BaseUnitTest() { verify(tracker, times(1)).trackSiteCreationAccessed(SiteCreationSource.UNSPECIFIED) } + @Test + fun `given instance state returns an invalid step, when start, then site creation is reset`() { + val expectedState = SiteCreationState(segmentId = SEGMENT_ID) + whenever(savedInstanceState.getParcelableCompat(KEY_SITE_CREATION_STATE)) + .thenReturn(expectedState) + whenever(savedInstanceState.getInt(KEY_CURRENT_STEP)).thenReturn(-1) // Invalid step + + val newViewModel = getNewViewModel() + newViewModel.start(savedInstanceState, SiteCreationSource.UNSPECIFIED) + + assertEquals(0, wizardManager.currentStep) + } + private fun currentWizardState(vm: SiteCreationMainVM) = vm.navigationTargetObservable.lastEvent!!.wizardState private fun getNewViewModel() = SiteCreationMainVM( 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 b3f02fdfa0b8..fc3c95e7fe33 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 @@ -312,7 +312,7 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { viewModel.onCreateSiteBtnClicked() - verify(tracker).trackDomainSelected(selectedDomain.domainName, "", expectedCost) + verify(tracker).trackDomainSelected(selectedDomain.domainName, "", expectedCost, true) } @Test @@ -324,7 +324,7 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { viewModel.onCreateSiteBtnClicked() - verify(tracker).trackDomainSelected(selectedDomain.domainName, "", expectedCost) + verify(tracker).trackDomainSelected(selectedDomain.domainName, "", expectedCost, false) } @Test 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 36562fb40408..494f13d07c45 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 @@ -785,6 +785,20 @@ class WPMainActivityViewModelTest : BaseUnitTest() { verify(observer, times(1)).onChanged(anyOrNull()) } + @Test + fun `requests my site dashboard refresh when requestMySiteDashboardRefresh is called`() { + startViewModelWithDefaultParameters() + + var observerCalledCount = 0 + viewModel.mySiteDashboardRefreshRequested.observeForever { + observerCalledCount++ + } + + viewModel.requestMySiteDashboardRefresh() + + assertThat(observerCalledCount).isEqualTo(1) + } + private fun startViewModelWithDefaultParameters( isWhatsNewFeatureEnabled: Boolean = true, isCreateFabEnabled: Boolean = true, diff --git a/build.gradle b/build.gradle index 062bee2efb0d..8d8026acf237 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,9 @@ +import com.automattic.android.measure.MeasureBuildsExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id "io.gitlab.arturbosch.detekt" + id 'com.automattic.android.measure-builds' id "androidx.navigation.safeargs.kotlin" apply false id "com.android.library" apply false id 'com.google.gms.google-services' apply false @@ -20,9 +22,9 @@ ext { automatticRestVersion = '1.0.8' automatticStoriesVersion = '2.4.0' automatticTracksVersion = '3.3.0' - gutenbergMobileVersion = 'v1.109.1' - wordPressAztecVersion = 'v1.8.0' - wordPressFluxCVersion = 'trunk-cdc8effb2affbb1c6bcf01f6606c847f87071d28' + gutenbergMobileVersion = 'v1.109.2' + wordPressAztecVersion = 'v1.9.0' + wordPressFluxCVersion = 'trunk-b4e51008f7200eba1dfdd22ca370fce450ce93b0' wordPressLoginVersion = '1.10.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.10.0' @@ -37,7 +39,6 @@ ext { androidxAnnotationVersion = '1.6.0' androidxAppcompatVersion = '1.6.1' androidxArchCoreVersion = '2.2.0' - androidxCameraVersion = '1.2.3' androidxComposeBomVersion = '2023.10.00' androidxComposeCompilerVersion = '1.5.3' androidxCardviewVersion = '1.0.0' @@ -70,8 +71,6 @@ ext { googleGsonVersion = '2.10.1' googleMaterialVersion = '1.9.0' - mlkitBarcodeScanningVersion = '17.2.0' - mlkitTextRecognitionVersion = '16.0.0' googleMLKitBarcodeScanningVersion = '17.0.0' googlePlayServicesAuthVersion = '20.4.1' googlePlayServicesCodeScannerVersion = '16.0.0-beta3' @@ -109,6 +108,13 @@ ext { wordPressLintVersion = '2.0.0' } +measureBuilds { + enable = findProperty('measureBuildsEnabled')?.toBoolean() ?: false + automatticProject = MeasureBuildsExtension.AutomatticProject.WordPress + authToken = findProperty('appsMetricsToken') + attachGradleScanId = System.getenv('CI')?.toBoolean() ?: false +} + allprojects { apply plugin: 'checkstyle' apply plugin: 'io.gitlab.arturbosch.detekt' diff --git a/docs/converting-to-kotlin.md b/docs/converting-to-kotlin.md index 3904c5e06292..0db3d34daa52 100644 --- a/docs/converting-to-kotlin.md +++ b/docs/converting-to-kotlin.md @@ -14,11 +14,17 @@ When converting a file from Java to Kotlin, ensure you enable this option in And 1. The first commit will involve a simple rename from `.java` to `.kt`. 2. The second commit will rename the `.kt` extension back to .java, followed by the actual commit. -**Reason**: Enabling this option helps the reviewer perform a diff on the second commit, showing precisely what changes occurred between the Java and Kotlin files. Otherwise, the PR will display a file deletion (`.java` file) and a file addition (`.kt` file), making it challenging for the reviewer to diff effectively. +**Reason:** Enabling this option helps the reviewer perform a diff on the second commit, showing precisely what changes occurred between the Java and Kotlin files. Otherwise, the PR will display a file deletion (`.java` file) and a file addition (`.kt` file), making it challenging for the reviewer to diff effectively. + +### Consider adding nullability annotations before initiating automatic conversion: + +Address the warnings of "Missing null annotation" before starting automatic conversion. + +**Reason:** This makes automatic conversion to handle nullability instead of setting everything to nullable. ### Depend on automatic conversion and commit immediately: When performing the conversion, rely on the automatic conversion tools and commit the changes promptly, even if the resulting Kotlin code doesn't compile. Subsequently, on another commit, you can refine the Kotlin code, making it more idiomatic or addressing any compilation issues, such as adding nullable checks (`!!` or `let`). -**Reason**: This approach informs the reviewer and other readers that the first commit involved an automated conversion without manual intervention. It helps establish that any code refinements occurred separately, providing clarity on the development process. +**Reason:** This approach informs the reviewer and other readers that the first commit involved an automated conversion without manual intervention. It helps establish that any code refinements occurred separately, providing clarity on the development process. diff --git a/fastlane/jetpack_metadata/android/ar/changelogs/1385.txt b/fastlane/jetpack_metadata/android/ar/changelogs/1385.txt deleted file mode 100644 index c252a6e762fa..000000000000 --- a/fastlane/jetpack_metadata/android/ar/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- تتيح لك شاشة كل النطاقات إدارة كل نطاقاتك في التطبيق. -- حسَّنت التدوينات والصفحات خيارات العرض وقائمة السياق الموسعة. -- أصلحنا مشكلات في محرر المكوّنات ناتجة عن لصق محتوى متداخل بشدة أو استخدام ألوان النص في قوالب الموقع القديمة. diff --git a/fastlane/jetpack_metadata/android/ar/changelogs/1392.txt b/fastlane/jetpack_metadata/android/ar/changelogs/1392.txt new file mode 100644 index 000000000000..966a87f15696 --- /dev/null +++ b/fastlane/jetpack_metadata/android/ar/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- يعرض محرر المكوّن خيار "إلغاء التجميع" فقط للمكوّنات المتداخلة التي تدعمه. +- يستخدم إعداد تحسين الصور الحجم والجودة المثاليين افتراضيًا. +- يتم تثبيت القوالب بشكل صحيح في المواقع المسجَّلة على خطتي الأعمال والتجارة. diff --git a/fastlane/jetpack_metadata/android/de-DE/changelogs/1385.txt b/fastlane/jetpack_metadata/android/de-DE/changelogs/1385.txt deleted file mode 100644 index 8f081554e27f..000000000000 --- a/fastlane/jetpack_metadata/android/de-DE/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- Im Bildschirm „Alle Domains“ kannst du alle deine Domains in der App verwalten. -- „Beiträge & Seiten“ verfügt jetzt über verbesserte Anzeigeoptionen und ein erweitertes Kontextmenü. -- Wir haben Probleme mit dem Block-Editor behoben, die durch das Einfügen tief verschachtelter Inhalte oder dem Verwenden von Textfarben in älteren Website-Themes entstanden sind. diff --git a/fastlane/jetpack_metadata/android/de-DE/changelogs/1392.txt b/fastlane/jetpack_metadata/android/de-DE/changelogs/1392.txt new file mode 100644 index 000000000000..3eba72dd8702 --- /dev/null +++ b/fastlane/jetpack_metadata/android/de-DE/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Der Block-Editor zeigt die Option „Gruppierung aufheben“ nur für verschachtelte Blöcke, die diese Option unterstützen. +- Die Einstellung „Bilder optimieren“ nutzt standardmäßig die optimale Größe und Qualität. +- Themes werden für Websites, die Business- und Commerce-Tarife verwenden, ordnungsgemäß installiert. diff --git a/fastlane/jetpack_metadata/android/en-US/changelogs/1385.txt b/fastlane/jetpack_metadata/android/en-US/changelogs/1385.txt deleted file mode 100644 index bda0409cf9f4..000000000000 --- a/fastlane/jetpack_metadata/android/en-US/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -- The All Domains screen lets you manage all your domains in the app. -- Posts & Pages has improved display options and an expanded context menu. -- We fixed block editor issues caused by pasting deeply nested content, or using text colors in older site themes. diff --git a/fastlane/jetpack_metadata/android/en-US/changelogs/1392.txt b/fastlane/jetpack_metadata/android/en-US/changelogs/1392.txt new file mode 100644 index 000000000000..c1beb73308d2 --- /dev/null +++ b/fastlane/jetpack_metadata/android/en-US/changelogs/1392.txt @@ -0,0 +1,3 @@ +- Block editor only shows the “ungroup” option for nested blocks that support it. +- Optimize Images setting uses optimal size and quality by default. +- Themes install properly for sites on Business and Commerce plans. diff --git a/fastlane/jetpack_metadata/android/es-ES/changelogs/1385.txt b/fastlane/jetpack_metadata/android/es-ES/changelogs/1385.txt deleted file mode 100644 index 30a900c14102..000000000000 --- a/fastlane/jetpack_metadata/android/es-ES/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23,7: -- La pantalla Todos los dominios te permite gestionar todos tus dominios en la aplicación. -- En Entradas y páginas se han mejorado las opciones de visualización y se ha ampliado el menú contextual. -- Se han solucionado los problemas del editor de bloques causados por el pegado de contenido muy anidado o por el uso de colores de texto en temas de sitios antiguos. diff --git a/fastlane/jetpack_metadata/android/es-ES/changelogs/1392.txt b/fastlane/jetpack_metadata/android/es-ES/changelogs/1392.txt new file mode 100644 index 000000000000..e744aab56775 --- /dev/null +++ b/fastlane/jetpack_metadata/android/es-ES/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +-El Editor de bloques solo muestra la opción "desagrupar" para los bloques anidados que le dan soporte. +- El ajuste Optimizar imágenes usa el tamaño y la calidad óptimos por defecto. +- Los temas se instalan correctamente en los sitios con planes Business y Commerce. diff --git a/fastlane/jetpack_metadata/android/fr-FR/changelogs/1385.txt b/fastlane/jetpack_metadata/android/fr-FR/changelogs/1385.txt deleted file mode 100644 index 33c3522cd2e7..000000000000 --- a/fastlane/jetpack_metadata/android/fr-FR/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7 : -- L’écran Tous les domaines vous permet de gérer tous vos domaines dans l’application. -- La section Articles et Pages bénéficie de meilleures options d’affichage et d’un menu contextuel étendu. -- Nous avons corrigé des erreurs dans l’éditeur de blocs engendrées par le coller de contenu profondément imbriqué ou l’utilisation de couleurs de texte dans des anciens thèmes de site. diff --git a/fastlane/jetpack_metadata/android/fr-FR/changelogs/1392.txt b/fastlane/jetpack_metadata/android/fr-FR/changelogs/1392.txt new file mode 100644 index 000000000000..840279f12fa9 --- /dev/null +++ b/fastlane/jetpack_metadata/android/fr-FR/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8 : +- L’éditeur de blocs affiche l’option « Dégrouper » uniquement pour les blocs imbriqués qui la prennent en charge. +- Le réglage Optimiser les images utilise la taille et la qualité optimales par défaut. +- Les thèmes s’installent correctement pour les sites disposant de plans Business et Commerce. diff --git a/fastlane/jetpack_metadata/android/id/changelogs/1385.txt b/fastlane/jetpack_metadata/android/id/changelogs/1385.txt deleted file mode 100644 index 74e04688732d..000000000000 --- a/fastlane/jetpack_metadata/android/id/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- Lewat layar Semua Domain, Anda bisa mengelola semua domain langsung dari aplikasi. -- Di layar Pos & Halaman, opsi tampilan telah disempurnakan dan menu konteks kini lebih lengkap. -- Error di editor blok akibat menempel konten yang lapisannya jauh di bawah atau akibat mengaktifkan warna teks di tema situs lawas sudah kami atasi. diff --git a/fastlane/jetpack_metadata/android/id/changelogs/1392.txt b/fastlane/jetpack_metadata/android/id/changelogs/1392.txt new file mode 100644 index 000000000000..fb6a341d1604 --- /dev/null +++ b/fastlane/jetpack_metadata/android/id/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Editor blok hanya menampilkan pilihan “batalkan pengelompokan” untuk blok bertingkat yang mendukung pilihan tersebut. +- Pengaturan Optimalkan Gambar menerapkan ukuran dan kualitas optimal secara default. +- Tema terinstal dengan benar untuk situs pada paket Bisnis dan Commerce. diff --git a/fastlane/jetpack_metadata/android/it-IT/changelogs/1385.txt b/fastlane/jetpack_metadata/android/it-IT/changelogs/1385.txt deleted file mode 100644 index 4cefb59cc552..000000000000 --- a/fastlane/jetpack_metadata/android/it-IT/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- La schermata Tutti i domini consente di gestire tutti i domini nell'app. -- Articoli e pagine offre opzioni di visualizzazione ottimizzate e un menu contestuale espanso. -- Sono stati corretti alcuni problemi correlati all'editor a blocchi che si verificavano quando venivano incollati contenuti con molti livelli di nidificazione o venivano usati colori del testo nei temi del sito precedenti. diff --git a/fastlane/jetpack_metadata/android/it-IT/changelogs/1392.txt b/fastlane/jetpack_metadata/android/it-IT/changelogs/1392.txt new file mode 100644 index 000000000000..ed362e0d27a8 --- /dev/null +++ b/fastlane/jetpack_metadata/android/it-IT/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- L'editor a blocchi mostra l'opzione "Non raggruppare" solo per i blocchi nidificati che supportano tale opzione. +- L'impostazione Ottimizza le immagini utilizza dimensioni e qualità ottimali per impostazione predefinita. +- I temi si installano correttamente sui siti con i piani Business e Commerce. diff --git a/fastlane/jetpack_metadata/android/iw-IL/changelogs/1385.txt b/fastlane/jetpack_metadata/android/iw-IL/changelogs/1385.txt deleted file mode 100644 index 4e04c165343b..000000000000 --- a/fastlane/jetpack_metadata/android/iw-IL/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- במסך 'כל הדומיינים' ניתן לנהל את כל הדומיינים שלך באפליקציה. -- ב'פוסטים ועמודים' יש אפשרויות תצוגה משופרות ותפריט הקשר מורחב. -- פתרנו בעיות בעורך הבלוקים, שנגרמו מהדבקה של תוכן בקינון עמוק או משימוש בצבעי טקסט בערכות עיצוב ותיקות יותר באתר. diff --git a/fastlane/jetpack_metadata/android/iw-IL/changelogs/1392.txt b/fastlane/jetpack_metadata/android/iw-IL/changelogs/1392.txt new file mode 100644 index 000000000000..a0defa14eb0d --- /dev/null +++ b/fastlane/jetpack_metadata/android/iw-IL/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +– בעורך הבלוקים האפשרות 'לבטל הקבצה', תוצג עבור בלוקים מקוננים רק אם הם אכן תומכים בכך. +– ההגדרה 'מיטוב תמונות' , ממטבת גודל ואיכות של תמונות כברירת מחדל. +– התקנה של ערכות עיצוב באתרים המנויים על תוכניות לעסקים ולמסחר נעשית באופן תקין. diff --git a/fastlane/jetpack_metadata/android/ja-JP/changelogs/1385.txt b/fastlane/jetpack_metadata/android/ja-JP/changelogs/1385.txt deleted file mode 100644 index cbd64bd9f4f6..000000000000 --- a/fastlane/jetpack_metadata/android/ja-JP/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- 「すべてのドメイン」画面ではアプリ内のすべてのドメインを管理できます。 -- 「投稿とページ」では、表示オプションが改善され、コンテキストメニューが拡張されました。 -- 深くネストされたコンテンツを貼り付けたり、古いサイトテーマでテキストの色を使用したりすることで発生するブロックエディターの問題を修正しました。 diff --git a/fastlane/jetpack_metadata/android/ja-JP/changelogs/1392.txt b/fastlane/jetpack_metadata/android/ja-JP/changelogs/1392.txt new file mode 100644 index 000000000000..7cc463d7cba6 --- /dev/null +++ b/fastlane/jetpack_metadata/android/ja-JP/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- ブロックエディターには、エディターをサポートするネストされたブロックの「グループ解除」オプションのみが表示されます。 +- 「画像を最適化」設定ではデフォルトで最適なサイズと品質が使用されます。 +- テーマはビジネスプランとコマースプランのサイトに適切にインストールされます。 diff --git a/fastlane/jetpack_metadata/android/ko-KR/changelogs/1385.txt b/fastlane/jetpack_metadata/android/ko-KR/changelogs/1385.txt deleted file mode 100644 index 9cd35aa97baa..000000000000 --- a/fastlane/jetpack_metadata/android/ko-KR/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- 모든 도메인 화면에서 앱의 도메인을 모두 관리할 수 있습니다. -- 글 및 페이지에 개선된 표시 옵션과 확장된 컨텍스트 메뉴가 있습니다. -- 깊숙이 중첩된 콘텐츠를 붙여 넣거나 오래된 사이트 테마에서 텍스트 색상을 사용하면 발생하는 블록 편집기 문제를 해결했습니다. diff --git a/fastlane/jetpack_metadata/android/ko-KR/changelogs/1392.txt b/fastlane/jetpack_metadata/android/ko-KR/changelogs/1392.txt new file mode 100644 index 000000000000..6a06ae119237 --- /dev/null +++ b/fastlane/jetpack_metadata/android/ko-KR/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- 중첩된 블록에서 "그룹 해제" 옵션을 지원하는 경우 해당 옵션만 블록 편집기에 표시됩니다. +- 이미지 최적화 설정에서 기본적으로 최적 크기와 품질을 사용합니다. +- 비즈니스 및 상거래 요금제의 사이트에 테마가 제대로 설치됩니다. diff --git a/fastlane/jetpack_metadata/android/nl-NL/changelogs/1385.txt b/fastlane/jetpack_metadata/android/nl-NL/changelogs/1385.txt deleted file mode 100644 index a71f7d5e1f5e..000000000000 --- a/fastlane/jetpack_metadata/android/nl-NL/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- In het scherm Alle domeinen kan je al je domeinen in de app beheren. -- Berichten en Pagina's heeft verbeterde weergaveopties en een uitgebreid contextmenu. -- We hebben problemen met de blokeditor opgelost door werden veroorzaak door het plakken van diepgeneste content of het gebruiken van tekstkleuren in oudere sitethema's. diff --git a/fastlane/jetpack_metadata/android/nl-NL/changelogs/1392.txt b/fastlane/jetpack_metadata/android/nl-NL/changelogs/1392.txt new file mode 100644 index 000000000000..a4e199fe9886 --- /dev/null +++ b/fastlane/jetpack_metadata/android/nl-NL/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Blokeditor toont alleen de optie 'Groepering opheffen' voor genestelde blokken die dat ondersteunen. +- De instelling voor Afbeeldingen optimaliseren gebruikt standaard de optimale afmetingen en kwaliteit. +- Thema's worden correct geïnstalleerd voor sites met Business- en Commerce-abonnementen. diff --git a/fastlane/jetpack_metadata/android/pt-BR/changelogs/1385.txt b/fastlane/jetpack_metadata/android/pt-BR/changelogs/1385.txt deleted file mode 100644 index 9ebf1d36e012..000000000000 --- a/fastlane/jetpack_metadata/android/pt-BR/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- A tela Todos os domínios permite que você gerencie todos os seus domínios no app. -- A seção Posts e páginas conta com opções de exibição melhoradas e um menu de contexto expandido. -- Corrigimos problemas no editor de blocos causados ao colar conteúdo bastante aninhado ou ao usar cores de texto em temas de sites mais antigos. diff --git a/fastlane/jetpack_metadata/android/pt-BR/changelogs/1392.txt b/fastlane/jetpack_metadata/android/pt-BR/changelogs/1392.txt new file mode 100644 index 000000000000..47cc1c84bf66 --- /dev/null +++ b/fastlane/jetpack_metadata/android/pt-BR/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- O editor de blocos só mostra a opção "Desagrupar" para blocos aninhados compatíveis com essa função. +- A configuração "Otimizar imagens" usa tamanho e qualidade otimizados por padrão. +- Os temas são instalados de forma adequada para sites nos planos Negócios e Commerce. diff --git a/fastlane/jetpack_metadata/android/ru-RU/changelogs/1385.txt b/fastlane/jetpack_metadata/android/ru-RU/changelogs/1385.txt deleted file mode 100644 index 143b8294f4c3..000000000000 --- a/fastlane/jetpack_metadata/android/ru-RU/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- На экране «Все домены» можно управлять всеми доменами в приложении. -- В разделе «Записи и страницы» появились улучшенные опции отображения и расширенное контекстное меню. -- Мы исправили ошибки в редакторе блоков при вставке глубоко вложенного контента или при использовании цветов текста в старых темах сайта. diff --git a/fastlane/jetpack_metadata/android/ru-RU/changelogs/1392.txt b/fastlane/jetpack_metadata/android/ru-RU/changelogs/1392.txt new file mode 100644 index 000000000000..9bb1b0e7f020 --- /dev/null +++ b/fastlane/jetpack_metadata/android/ru-RU/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Опция «Разгруппировать» отображается в редакторе блоков только для тех вложенных блоков, которые её поддерживают. +- Настройка «Оптимизировать изображение» по умолчанию устанавливает для изображения оптимальные размер и качество. +- На сайтах, работающих по тарифным планам Business и Commerce, темы устанавливаются правильно. diff --git a/fastlane/jetpack_metadata/android/sv-SE/changelogs/1385.txt b/fastlane/jetpack_metadata/android/sv-SE/changelogs/1385.txt deleted file mode 100644 index 3414cc086ce1..000000000000 --- a/fastlane/jetpack_metadata/android/sv-SE/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- På skärmen Alla domäner kan du hantera alla dina domäner i appen. -- Inlägg och sidor har förbättrade visningsalternativ och en utökad kontextmeny. -- Vi har åtgärdat problem med blockredigeraren som orsakats av att man klistrat in djupt inbäddat innehåll eller använt textfärger från äldre webbplatsteman. diff --git a/fastlane/jetpack_metadata/android/sv-SE/changelogs/1392.txt b/fastlane/jetpack_metadata/android/sv-SE/changelogs/1392.txt new file mode 100644 index 000000000000..6950684baf9d --- /dev/null +++ b/fastlane/jetpack_metadata/android/sv-SE/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Blockredigeraren visar endast alternativet "avgruppera" för inbäddade block som stöder detta. +- Inställningen Optimera bilder använder optimal storlek och kvalitet som standard. +- Teman installeras korrekt för webbplatser med Business- och Commerce-paket. diff --git a/fastlane/jetpack_metadata/android/tr-TR/changelogs/1385.txt b/fastlane/jetpack_metadata/android/tr-TR/changelogs/1385.txt deleted file mode 100644 index d8c09c46b85a..000000000000 --- a/fastlane/jetpack_metadata/android/tr-TR/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- Tüm Alan Adları ekranı, tüm alan adlarınızı uygulamada yönetmenizi sağlar. -- Gönderiler ve Sayfalarda iyileştirilmiş görüntüleme seçenekleri ve genişletilmiş bir bağlam menüsü bulunur. -- Derin şekilde iç içe geçmiş içerik yapıştırmaktan veya daha eski site temalarında metin renkleri kullanmaktan kaynaklanan blok düzenleyici sorunlarını düzelttik. diff --git a/fastlane/jetpack_metadata/android/tr-TR/changelogs/1392.txt b/fastlane/jetpack_metadata/android/tr-TR/changelogs/1392.txt new file mode 100644 index 000000000000..fa0bc7812374 --- /dev/null +++ b/fastlane/jetpack_metadata/android/tr-TR/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Blok düzenleyici "grubu ayır" seçeneğini yalnızca bu seçeneği destekleyen iç içe geçmiş bloklar için gösterir. +- Görselleri İyileştirme ayarı varsayılan olarak ideal boyut ve kaliteyi kullanır. +- Kurumsal pakette ve Ticaret paketinde temalar sitelerde düzgün yüklenir. diff --git a/fastlane/jetpack_metadata/android/zh-CN/changelogs/1385.txt b/fastlane/jetpack_metadata/android/zh-CN/changelogs/1385.txt deleted file mode 100644 index 2ec6e667ab90..000000000000 --- a/fastlane/jetpack_metadata/android/zh-CN/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: -- 您可以在应用程序中通过“所有域名”界面管理您的所有域名。 -- “文章和页面”界面改进了显示选项,并扩展了上下文菜单。 -- 我们修复了因粘贴深度嵌套内容或使用旧站点主题中的文本颜色而导致的区块编辑器问题。 diff --git a/fastlane/jetpack_metadata/android/zh-CN/changelogs/1392.txt b/fastlane/jetpack_metadata/android/zh-CN/changelogs/1392.txt new file mode 100644 index 000000000000..fc7127fbc6a0 --- /dev/null +++ b/fastlane/jetpack_metadata/android/zh-CN/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- 在区块编辑器中,仅支持“取消分组”选项的嵌套区块会显示该选项。 +- 优化图像设置为默认使用最佳尺寸和质量。 +- 采用商务版和电子商务版套餐的站点可正常安装主题。 diff --git a/fastlane/jetpack_metadata/android/zh-TW/changelogs/1385.txt b/fastlane/jetpack_metadata/android/zh-TW/changelogs/1385.txt deleted file mode 100644 index ee38c54de641..000000000000 --- a/fastlane/jetpack_metadata/android/zh-TW/changelogs/1385.txt +++ /dev/null @@ -1,4 +0,0 @@ -23.7: --「所以網域」畫面可讓你在應用程式中管理所有網域。 --「文章與頁面」已改善顯示選項和展開的內容選單。 -- 我們修正了因貼上深層巢狀內容或在舊網站主題使用文字顏色而導致的區塊編輯器問題。 diff --git a/fastlane/jetpack_metadata/android/zh-TW/changelogs/1392.txt b/fastlane/jetpack_metadata/android/zh-TW/changelogs/1392.txt new file mode 100644 index 000000000000..51ef47bab3da --- /dev/null +++ b/fastlane/jetpack_metadata/android/zh-TW/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- 只有區塊編輯器中支援「取消群組」功能的內嵌區塊,才會顯示對應選項。 +- 「最佳化圖片」設定預設為最佳大小和畫質。 +- 商用版和電子商務版方案網站可正常安裝佈景主題。 diff --git a/fastlane/metadata/android/ar/changelogs/1385.txt b/fastlane/metadata/android/ar/changelogs/1385.txt deleted file mode 100644 index 6741c660100c..000000000000 --- a/fastlane/metadata/android/ar/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- يوجد الآن في شاشة التدوينات والصفحات خيارات عرض بسيطة وجذابة. تتيح لك قائمة السياق أيضًا الوصول إلى التعليقات والإعدادات وغيرها من الإجراءات الخاصة بتدوينة أو صفحة. -- أصلحنا مشكلات في محرر المكوّنات ناتجة عن لصق محتوى متداخل بشدة أو استخدام ألوان النص في قوالب الموقع القديمة. diff --git a/fastlane/metadata/android/ar/changelogs/1392.txt b/fastlane/metadata/android/ar/changelogs/1392.txt new file mode 100644 index 000000000000..966a87f15696 --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- يعرض محرر المكوّن خيار "إلغاء التجميع" فقط للمكوّنات المتداخلة التي تدعمه. +- يستخدم إعداد تحسين الصور الحجم والجودة المثاليين افتراضيًا. +- يتم تثبيت القوالب بشكل صحيح في المواقع المسجَّلة على خطتي الأعمال والتجارة. diff --git a/fastlane/metadata/android/de-DE/changelogs/1385.txt b/fastlane/metadata/android/de-DE/changelogs/1385.txt deleted file mode 100644 index c53b987e7023..000000000000 --- a/fastlane/metadata/android/de-DE/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- Der Bildschirm „Beiträge & Seiten“ enthält nun schlichte, einfache Anzeigeoptionen. Im Kontextmenü kannst du auch auf Kommentare, Einstellungen und andere Aktionen für einen Beitrag oder eine Seite zugreifen. -- Wir haben Probleme mit dem Block-Editor behoben, die durch das Einfügen tief verschachtelter Inhalte oder dem Verwenden von Textfarben in älteren Website-Themes entstanden sind. diff --git a/fastlane/metadata/android/de-DE/changelogs/1392.txt b/fastlane/metadata/android/de-DE/changelogs/1392.txt new file mode 100644 index 000000000000..3eba72dd8702 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Der Block-Editor zeigt die Option „Gruppierung aufheben“ nur für verschachtelte Blöcke, die diese Option unterstützen. +- Die Einstellung „Bilder optimieren“ nutzt standardmäßig die optimale Größe und Qualität. +- Themes werden für Websites, die Business- und Commerce-Tarife verwenden, ordnungsgemäß installiert. diff --git a/fastlane/metadata/android/en-US/changelogs/1385.txt b/fastlane/metadata/android/en-US/changelogs/1385.txt deleted file mode 100644 index 1a2959724af9..000000000000 --- a/fastlane/metadata/android/en-US/changelogs/1385.txt +++ /dev/null @@ -1,2 +0,0 @@ -- The Posts & Pages screen now has clean, simple display options. The context menu also lets you access comments, settings, and other actions for a post or page. -- We fixed block editor issues caused by pasting deeply nested content, or using text colors in older site themes. diff --git a/fastlane/metadata/android/en-US/changelogs/1392.txt b/fastlane/metadata/android/en-US/changelogs/1392.txt new file mode 100644 index 000000000000..c1beb73308d2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1392.txt @@ -0,0 +1,3 @@ +- Block editor only shows the “ungroup” option for nested blocks that support it. +- Optimize Images setting uses optimal size and quality by default. +- Themes install properly for sites on Business and Commerce plans. diff --git a/fastlane/metadata/android/es-ES/changelogs/1385.txt b/fastlane/metadata/android/es-ES/changelogs/1385.txt deleted file mode 100644 index c6d641a53c39..000000000000 --- a/fastlane/metadata/android/es-ES/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- La pantalla de entradas y páginas tiene ahora opciones de visualización limpias y sencillas. El menú contextual también te permite acceder a comentarios, ajustes y otras acciones para una entrada o página. -- Hemos solucionado problemas del editor de bloques causados por pegar contenido anidado profundamente, o por utilizar colores de texto en temas de sitios antiguos. diff --git a/fastlane/metadata/android/es-ES/changelogs/1392.txt b/fastlane/metadata/android/es-ES/changelogs/1392.txt new file mode 100644 index 000000000000..e744aab56775 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +-El Editor de bloques solo muestra la opción "desagrupar" para los bloques anidados que le dan soporte. +- El ajuste Optimizar imágenes usa el tamaño y la calidad óptimos por defecto. +- Los temas se instalan correctamente en los sitios con planes Business y Commerce. diff --git a/fastlane/metadata/android/fr-CA/changelogs/1385.txt b/fastlane/metadata/android/fr-CA/changelogs/1385.txt deleted file mode 100644 index eef941f699e6..000000000000 --- a/fastlane/metadata/android/fr-CA/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7 : -- L’écran Articles et Pages a bénéficié d’un lifting et dispose désormais d’options d’affichage épurées et simples. Le menu contextuel vous permet aussi d’accéder aux commentaires, aux réglages et à d’autres actions relatives à un article ou une page. -- Nous avons corrigé des erreurs dans l’éditeur de blocs engendrées par le coller de contenu profondément imbriqué ou l’utilisation de couleurs de texte dans des anciens thèmes de site. diff --git a/fastlane/metadata/android/fr-CA/changelogs/1392.txt b/fastlane/metadata/android/fr-CA/changelogs/1392.txt new file mode 100644 index 000000000000..840279f12fa9 --- /dev/null +++ b/fastlane/metadata/android/fr-CA/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8 : +- L’éditeur de blocs affiche l’option « Dégrouper » uniquement pour les blocs imbriqués qui la prennent en charge. +- Le réglage Optimiser les images utilise la taille et la qualité optimales par défaut. +- Les thèmes s’installent correctement pour les sites disposant de plans Business et Commerce. diff --git a/fastlane/metadata/android/fr-FR/changelogs/1385.txt b/fastlane/metadata/android/fr-FR/changelogs/1385.txt deleted file mode 100644 index eef941f699e6..000000000000 --- a/fastlane/metadata/android/fr-FR/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7 : -- L’écran Articles et Pages a bénéficié d’un lifting et dispose désormais d’options d’affichage épurées et simples. Le menu contextuel vous permet aussi d’accéder aux commentaires, aux réglages et à d’autres actions relatives à un article ou une page. -- Nous avons corrigé des erreurs dans l’éditeur de blocs engendrées par le coller de contenu profondément imbriqué ou l’utilisation de couleurs de texte dans des anciens thèmes de site. diff --git a/fastlane/metadata/android/fr-FR/changelogs/1392.txt b/fastlane/metadata/android/fr-FR/changelogs/1392.txt new file mode 100644 index 000000000000..840279f12fa9 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8 : +- L’éditeur de blocs affiche l’option « Dégrouper » uniquement pour les blocs imbriqués qui la prennent en charge. +- Le réglage Optimiser les images utilise la taille et la qualité optimales par défaut. +- Les thèmes s’installent correctement pour les sites disposant de plans Business et Commerce. diff --git a/fastlane/metadata/android/id/changelogs/1385.txt b/fastlane/metadata/android/id/changelogs/1385.txt deleted file mode 100644 index 700817361bc9..000000000000 --- a/fastlane/metadata/android/id/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- Layar Pos & Halaman sekarang dilengkapi opsi tampilan yang simpel nan bersih. Mengakses komentar, pengaturan, dan opsi lain kini bisa dilakukan dari menu konteks pos atau halaman. -- Error di editor blok akibat menempel konten yang lapisannya jauh di bawah atau akibat mengaktifkan warna teks di tema situs lawas sudah kami atasi. diff --git a/fastlane/metadata/android/id/changelogs/1392.txt b/fastlane/metadata/android/id/changelogs/1392.txt new file mode 100644 index 000000000000..fb6a341d1604 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Editor blok hanya menampilkan pilihan “batalkan pengelompokan” untuk blok bertingkat yang mendukung pilihan tersebut. +- Pengaturan Optimalkan Gambar menerapkan ukuran dan kualitas optimal secara default. +- Tema terinstal dengan benar untuk situs pada paket Bisnis dan Commerce. diff --git a/fastlane/metadata/android/it-IT/changelogs/1385.txt b/fastlane/metadata/android/it-IT/changelogs/1385.txt deleted file mode 100644 index a87a03db4896..000000000000 --- a/fastlane/metadata/android/it-IT/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- La schermata Articoli e pagine ora offre opzioni di visualizzazione semplici e intuitive. Il menu contestuale consente anche di accedere a commenti, impostazioni e altre azioni da un articolo o una pagina. -- Sono stati corretti alcuni problemi correlati all'editor a blocchi che si verificavano quando venivano incollati contenuti con molti livelli di nidificazione o venivano usati colori del testo nei temi del sito precedenti. diff --git a/fastlane/metadata/android/it-IT/changelogs/1392.txt b/fastlane/metadata/android/it-IT/changelogs/1392.txt new file mode 100644 index 000000000000..ed362e0d27a8 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- L'editor a blocchi mostra l'opzione "Non raggruppare" solo per i blocchi nidificati che supportano tale opzione. +- L'impostazione Ottimizza le immagini utilizza dimensioni e qualità ottimali per impostazione predefinita. +- I temi si installano correttamente sui siti con i piani Business e Commerce. diff --git a/fastlane/metadata/android/iw-IL/changelogs/1385.txt b/fastlane/metadata/android/iw-IL/changelogs/1385.txt deleted file mode 100644 index 89a7e14047ed..000000000000 --- a/fastlane/metadata/android/iw-IL/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- מעכשיו, מסך 'פוסטים ועמודים' מכיל אפשרויות תצוגה פשוטות ונקיות. בנוסף, תפריט ההקשר מאפשר גישה לתגובות, להגדרות ולפעולות אחרות עבור פוסטים או עמודים. -- פתרנו בעיות בעורך הבלוקים, שנגרמו מהדבקה של תוכן בקינון עמוק או משימוש בצבעי טקסט בערכות עיצוב ותיקות יותר באתר. diff --git a/fastlane/metadata/android/iw-IL/changelogs/1392.txt b/fastlane/metadata/android/iw-IL/changelogs/1392.txt new file mode 100644 index 000000000000..a0defa14eb0d --- /dev/null +++ b/fastlane/metadata/android/iw-IL/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +– בעורך הבלוקים האפשרות 'לבטל הקבצה', תוצג עבור בלוקים מקוננים רק אם הם אכן תומכים בכך. +– ההגדרה 'מיטוב תמונות' , ממטבת גודל ואיכות של תמונות כברירת מחדל. +– התקנה של ערכות עיצוב באתרים המנויים על תוכניות לעסקים ולמסחר נעשית באופן תקין. diff --git a/fastlane/metadata/android/ja-JP/changelogs/1385.txt b/fastlane/metadata/android/ja-JP/changelogs/1385.txt deleted file mode 100644 index 0b56859e5bf2..000000000000 --- a/fastlane/metadata/android/ja-JP/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- 「投稿とページ」画面にクリーンでシンプルな表示オプションが追加されました。 - コンテキストメニューから、投稿またはページのコメント、設定、その他のアクションにもアクセスできるようになりました。 -- 深くネストされたコンテンツを貼り付けたり、古いサイトテーマでテキストの色を使用したりすることで発生するブロックエディターの問題を修正しました。 diff --git a/fastlane/metadata/android/ja-JP/changelogs/1392.txt b/fastlane/metadata/android/ja-JP/changelogs/1392.txt new file mode 100644 index 000000000000..7cc463d7cba6 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- ブロックエディターには、エディターをサポートするネストされたブロックの「グループ解除」オプションのみが表示されます。 +- 「画像を最適化」設定ではデフォルトで最適なサイズと品質が使用されます。 +- テーマはビジネスプランとコマースプランのサイトに適切にインストールされます。 diff --git a/fastlane/metadata/android/ko-KR/changelogs/1385.txt b/fastlane/metadata/android/ko-KR/changelogs/1385.txt deleted file mode 100644 index ddbb5d2415a5..000000000000 --- a/fastlane/metadata/android/ko-KR/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- 이제는 글 및 페이지 화면에 깨끗하고 간단한 표시 옵션이 있습니다. 컨텍스트 메뉴에서도 댓글, 설정 및 기타 글 또는 페이지 작업에 접근할 수 있습니다. -- 깊숙이 중첩된 콘텐츠를 붙여 넣거나 오래된 사이트 테마에서 텍스트 색상을 사용하면 발생하는 블록 편집기 문제를 해결했습니다. diff --git a/fastlane/metadata/android/ko-KR/changelogs/1392.txt b/fastlane/metadata/android/ko-KR/changelogs/1392.txt new file mode 100644 index 000000000000..6a06ae119237 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- 중첩된 블록에서 "그룹 해제" 옵션을 지원하는 경우 해당 옵션만 블록 편집기에 표시됩니다. +- 이미지 최적화 설정에서 기본적으로 최적 크기와 품질을 사용합니다. +- 비즈니스 및 상거래 요금제의 사이트에 테마가 제대로 설치됩니다. diff --git a/fastlane/metadata/android/nl-NL/changelogs/1385.txt b/fastlane/metadata/android/nl-NL/changelogs/1385.txt deleted file mode 100644 index 740ffc5c8447..000000000000 --- a/fastlane/metadata/android/nl-NL/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- Het scherm Berichten en pagina's heeft nu strakke, eenvoudige weergaveopties. Via het contextmenu heb je toegang tot opmerkingen, instellingen en andere acties voor een bericht of pagina. -- We hebben problemen met de blokeditor opgelost door werden veroorzaak door het plakken van diepgeneste content of het gebruiken van tekstkleuren in oudere sitethema's. diff --git a/fastlane/metadata/android/nl-NL/changelogs/1392.txt b/fastlane/metadata/android/nl-NL/changelogs/1392.txt new file mode 100644 index 000000000000..a4e199fe9886 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Blokeditor toont alleen de optie 'Groepering opheffen' voor genestelde blokken die dat ondersteunen. +- De instelling voor Afbeeldingen optimaliseren gebruikt standaard de optimale afmetingen en kwaliteit. +- Thema's worden correct geïnstalleerd voor sites met Business- en Commerce-abonnementen. diff --git a/fastlane/metadata/android/pt-BR/changelogs/1385.txt b/fastlane/metadata/android/pt-BR/changelogs/1385.txt deleted file mode 100644 index af11156aa645..000000000000 --- a/fastlane/metadata/android/pt-BR/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- A tela Posts e páginas agora tem opções de exibição claras e simples. O menu de contexto também permite acessar comentários, configurações e outras ações para um post ou página. -- Corrigimos problemas no editor de blocos causados ao colar conteúdo bastante aninhado ou ao usar cores de texto em temas de sites mais antigos. diff --git a/fastlane/metadata/android/ru-RU/changelogs/1385.txt b/fastlane/metadata/android/ru-RU/changelogs/1385.txt deleted file mode 100644 index 8a6ec1b01c95..000000000000 --- a/fastlane/metadata/android/ru-RU/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- У экрана «Записи и страницы» появились простые и понятные параметры отображения. - Из контекстного меню можно переходить к комментариям, настройкам и другим действиям по отношению к записи или странице. -- Мы исправили ошибки в редакторе блоков при вставке глубоко вложенного контента или при использовании цветов текста в старых темах сайта. diff --git a/fastlane/metadata/android/ru-RU/changelogs/1392.txt b/fastlane/metadata/android/ru-RU/changelogs/1392.txt new file mode 100644 index 000000000000..9bb1b0e7f020 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Опция «Разгруппировать» отображается в редакторе блоков только для тех вложенных блоков, которые её поддерживают. +- Настройка «Оптимизировать изображение» по умолчанию устанавливает для изображения оптимальные размер и качество. +- На сайтах, работающих по тарифным планам Business и Commerce, темы устанавливаются правильно. diff --git a/fastlane/metadata/android/sv-SE/changelogs/1385.txt b/fastlane/metadata/android/sv-SE/changelogs/1385.txt deleted file mode 100644 index 1708870a9bdc..000000000000 --- a/fastlane/metadata/android/sv-SE/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- Skärmen Inlägg och sidor har nu rena och enkla visningsalternativ. I kontextmenyn kan du även komma åt kommentarer, inställningar och andra åtgärder för ett inlägg eller en sida. -- Vi har åtgärdat problem med blockredigeraren som orsakats av att man klistrat in djupt inbäddat innehåll eller använt textfärger från äldre webbplatsteman. diff --git a/fastlane/metadata/android/sv-SE/changelogs/1392.txt b/fastlane/metadata/android/sv-SE/changelogs/1392.txt new file mode 100644 index 000000000000..6950684baf9d --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Blockredigeraren visar endast alternativet "avgruppera" för inbäddade block som stöder detta. +- Inställningen Optimera bilder använder optimal storlek och kvalitet som standard. +- Teman installeras korrekt för webbplatser med Business- och Commerce-paket. diff --git a/fastlane/metadata/android/tr-TR/changelogs/1385.txt b/fastlane/metadata/android/tr-TR/changelogs/1385.txt deleted file mode 100644 index a6d4106cd047..000000000000 --- a/fastlane/metadata/android/tr-TR/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- Gönderiler ve Sayfalar ekranında artık temiz ve basit görüntüleme seçenekleri bulunuyor. Bağlam menüsü ayrıca bir gönderinin veya sayfanın yorumlarına, ayarlarına ve diğer işlemlerine erişmenizi sağlar. -- Derin şekilde iç içe geçmiş içerik yapıştırmaktan veya daha eski site temalarında metin renkleri kullanmaktan kaynaklanan blok düzenleyici sorunlarını düzelttik. diff --git a/fastlane/metadata/android/tr-TR/changelogs/1392.txt b/fastlane/metadata/android/tr-TR/changelogs/1392.txt new file mode 100644 index 000000000000..fa0bc7812374 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- Blok düzenleyici "grubu ayır" seçeneğini yalnızca bu seçeneği destekleyen iç içe geçmiş bloklar için gösterir. +- Görselleri İyileştirme ayarı varsayılan olarak ideal boyut ve kaliteyi kullanır. +- Kurumsal pakette ve Ticaret paketinde temalar sitelerde düzgün yüklenir. diff --git a/fastlane/metadata/android/zh-CN/changelogs/1385.txt b/fastlane/metadata/android/zh-CN/changelogs/1385.txt deleted file mode 100644 index 4d445cfa2899..000000000000 --- a/fastlane/metadata/android/zh-CN/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- “文章和页面”界面现在的显示选项简洁明了。 您还可以通过上下文菜单访问文章或页面的评论、设置和其他操作项。 -- 我们修复了因粘贴深度嵌套内容或使用旧站点主题中的文本颜色而导致的区块编辑器问题。 diff --git a/fastlane/metadata/android/zh-CN/changelogs/1392.txt b/fastlane/metadata/android/zh-CN/changelogs/1392.txt new file mode 100644 index 000000000000..fc7127fbc6a0 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- 在区块编辑器中,仅支持“取消分组”选项的嵌套区块会显示该选项。 +- 优化图像设置为默认使用最佳尺寸和质量。 +- 采用商务版和电子商务版套餐的站点可正常安装主题。 diff --git a/fastlane/metadata/android/zh-TW/changelogs/1385.txt b/fastlane/metadata/android/zh-TW/changelogs/1385.txt deleted file mode 100644 index b889478c1e9e..000000000000 --- a/fastlane/metadata/android/zh-TW/changelogs/1385.txt +++ /dev/null @@ -1,3 +0,0 @@ -23.7: -- 「文章與頁面」現在提供乾淨、簡單的顯示選項。 - 現在,內容選單也可讓你存取文章與頁面的留言、設定及其他操作。 -- 我們修正了因貼上深層巢狀內容或在舊網站主題使用文字顏色而導致的區塊編輯器問題。 diff --git a/fastlane/metadata/android/zh-TW/changelogs/1392.txt b/fastlane/metadata/android/zh-TW/changelogs/1392.txt new file mode 100644 index 000000000000..51ef47bab3da --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/1392.txt @@ -0,0 +1,4 @@ +23.8: +- 只有區塊編輯器中支援「取消群組」功能的內嵌區塊,才會顯示對應選項。 +- 「最佳化圖片」設定預設為最佳大小和畫質。 +- 商用版和電子商務版方案網站可正常安裝佈景主題。 diff --git a/fastlane/resources/values/strings.xml b/fastlane/resources/values/strings.xml index da38d8cbe288..fbd2f5d81b5d 100644 --- a/fastlane/resources/values/strings.xml +++ b/fastlane/resources/values/strings.xml @@ -4803,19 +4803,40 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> - Scan Barcode - Camera permission is required to scan the barcode. - Grant Camera Permission - Camera permission is required in order to scan the barcode - You have permanently denied Camera permission. It is required in order to scan the barcode. Please enable it from the app settings - Grant - Cancel - Go to settings + Scan Barcode + Camera permission is required to scan the barcode. + Grant Camera Permission + Camera permission is required in order to scan the barcode + You have permanently denied Camera permission. It is required in order to scan the barcode. Please enable it from the app settings + Grant + Cancel + Go to settings Use a security key There was some trouble with the Security key login Please provide your security key to continue. + Alternatively, you can flatten the content by ungrouping the block. For this reason, we recommend editing the block using the web editor. For this reason, we recommend editing the block using your web browser. + + + Bloganuary is coming! + For the month of January, blogging prompts will come from Bloganuary - our community challenge to build a blogging habit for the new year. + @string/learn_more + Bloganuary + Join our month-long writing challenge + Receive a new prompt to inspire you each day. + Publish your response. + Read other bloggers’ responses to get inspiration and make new connections. + Bloganuary will use Daily Blogging Prompts to send you topics for the month of January. You have Blogging Prompts currently disabled. + Bloganuary will use Daily Blogging Prompts to send you topics for the month of January. + Turn on blogging prompts + Let’s go! + + + + State of the Word 2023 + Check out WordPress co-founder Matt Mullenweg’s annual keynote to stay on top of what’s coming in 2024 and beyond. + Watch now 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 eb0b815ee361..18a6ba18e538 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 @@ -379,7 +379,6 @@ public enum Stat { CLOSE_ACCOUNT_FAILED, CLOSED_ACCOUNT, ACCOUNT_LOGOUT, - SHARED_ITEM, SHARED_ITEM_READER, ADDED_SELF_HOSTED_SITE, SIGNED_IN, @@ -1096,6 +1095,13 @@ public enum Stat { BARCODE_SCANNING_SUCCESS, BARCODE_SCANNING_FAILURE, QRLOGIN_SCANNER_DISMISSED_CAMERA_PERMISSION_DENIED, + BLOGANUARY_NUDGE_MY_SITE_CARD_LEARN_MORE_TAPPED, + BLOGANUARY_NUDGE_LEARN_MORE_MODAL_SHOWN, + BLOGANUARY_NUDGE_LEARN_MORE_MODAL_DISMISSED, + BLOGANUARY_NUDGE_LEARN_MORE_MODAL_ACTION_TAPPED, + SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN, + SOTW_2023_NUDGE_POST_EVENT_CARD_HIDE_TAPPED, + SOTW_2023_NUDGE_POST_EVENT_CARD_CTA_TAPPED, } 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 62289e508b73..2c7e29fdc918 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 @@ -1031,8 +1031,6 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) { return "close_account_failed"; case CLOSED_ACCOUNT: return "closed_account"; - case SHARED_ITEM: - return "item_shared"; case SHARED_ITEM_READER: return "item_shared_reader"; case ADDED_SELF_HOSTED_SITE: @@ -2677,6 +2675,20 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) { return "barcode_scanning_failure"; case QRLOGIN_SCANNER_DISMISSED_CAMERA_PERMISSION_DENIED: return "qrlogin_scanner_dismissed_camera_permission_denied"; + case BLOGANUARY_NUDGE_MY_SITE_CARD_LEARN_MORE_TAPPED: + return "bloganuary_nudge_my_site_card_learn_more_tapped"; + case BLOGANUARY_NUDGE_LEARN_MORE_MODAL_SHOWN: + return "bloganuary_nudge_learn_more_modal_shown"; + case BLOGANUARY_NUDGE_LEARN_MORE_MODAL_DISMISSED: + return "bloganuary_nudge_learn_more_modal_dismissed"; + case BLOGANUARY_NUDGE_LEARN_MORE_MODAL_ACTION_TAPPED: + return "bloganuary_nudge_learn_more_modal_action_tapped"; + case SOTW_2023_NUDGE_POST_EVENT_CARD_SHOWN: + return "sotw_2023_nudge_post_event_card_shown"; + case SOTW_2023_NUDGE_POST_EVENT_CARD_HIDE_TAPPED: + return "sotw_2023_nudge_post_event_card_hide_tapped"; + case SOTW_2023_NUDGE_POST_EVENT_CARD_CTA_TAPPED: + return "sotw_2023_nudge_post_event_card_cta_tapped"; } return null; } 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 04c4b14ebc06..165d85414908 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 @@ -1,6 +1,7 @@ package org.wordpress.android.editor.gutenberg; import android.app.Activity; +import android.content.Context; import android.os.Bundle; import android.view.ViewGroup; @@ -14,6 +15,7 @@ import org.wordpress.android.editor.BuildConfig; import org.wordpress.android.editor.ExceptionLogger; import org.wordpress.android.editor.R; +import org.wordpress.android.editor.savedinstance.SavedInstanceDatabase; import org.wordpress.mobile.WPAndroidGlue.ShowSuggestionsUtil; import org.wordpress.mobile.WPAndroidGlue.GutenbergProps; import org.wordpress.mobile.WPAndroidGlue.RequestExecutor; @@ -53,11 +55,12 @@ public class GutenbergContainerFragment extends Fragment { private boolean mHasReceivedAnyContent; private WPAndroidGlueCode mWPAndroidGlueCode; - public static GutenbergContainerFragment newInstance(GutenbergPropsBuilder gutenbergPropsBuilder) { + public static GutenbergContainerFragment newInstance(Context context, GutenbergPropsBuilder gutenbergPropsBuilder) { GutenbergContainerFragment fragment = new GutenbergContainerFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_GUTENBERG_PROPS_BUILDER, gutenbergPropsBuilder); - fragment.setArguments(args); + SavedInstanceDatabase db = SavedInstanceDatabase.Companion.getDatabase(context); + if (db != null) { + db.addParcel(ARG_GUTENBERG_PROPS_BUILDER, gutenbergPropsBuilder); + } return fragment; } @@ -124,7 +127,11 @@ public void attachToContainer(ViewGroup viewGroup, OnMediaLibraryButtonListener public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - GutenbergPropsBuilder gutenbergPropsBuilder = getArguments().getParcelable(ARG_GUTENBERG_PROPS_BUILDER); + GutenbergPropsBuilder gutenbergPropsBuilder = null; + SavedInstanceDatabase db = SavedInstanceDatabase.Companion.getDatabase(getContext()); + if (db != null) { + gutenbergPropsBuilder = db.getParcel(ARG_GUTENBERG_PROPS_BUILDER, GutenbergPropsBuilder.CREATOR); + } Consumer exceptionLogger = null; Consumer breadcrumbLogger = null; 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 34595c07a77d..ba97a632f6a0 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 @@ -47,6 +47,7 @@ import org.wordpress.android.editor.EditorThemeUpdateListener; import org.wordpress.android.editor.LiveTextWatcher; import org.wordpress.android.editor.R; +import org.wordpress.android.editor.savedinstance.SavedInstanceDatabase; import org.wordpress.android.editor.WPGutenbergWebViewActivity; import org.wordpress.android.editor.gutenberg.GutenbergDialogFragment.GutenbergDialogNegativeClickInterface; import org.wordpress.android.editor.gutenberg.GutenbergDialogFragment.GutenbergDialogPositiveClickInterface; @@ -161,8 +162,7 @@ public class GutenbergEditorFragment extends EditorFragmentAbstract implements private ProgressDialog mSavingContentProgressDialog; - public static GutenbergEditorFragment newInstance(String title, - String content, + public static GutenbergEditorFragment newInstance(Context context, boolean isNewPost, GutenbergWebViewAuthorizationData webViewAuthorizationData, GutenbergPropsBuilder gutenbergPropsBuilder, @@ -170,14 +170,15 @@ public static GutenbergEditorFragment newInstance(String title, boolean jetpackFeaturesEnabled) { GutenbergEditorFragment fragment = new GutenbergEditorFragment(); Bundle args = new Bundle(); - args.putString(ARG_PARAM_TITLE, title); - args.putString(ARG_PARAM_CONTENT, content); args.putBoolean(ARG_IS_NEW_POST, isNewPost); - args.putParcelable(ARG_GUTENBERG_WEB_VIEW_AUTH_DATA, webViewAuthorizationData); - args.putParcelable(ARG_GUTENBERG_PROPS_BUILDER, gutenbergPropsBuilder); args.putInt(ARG_STORY_EDITOR_REQUEST_CODE, storyBlockEditRequestCode); args.putBoolean(ARG_JETPACK_FEATURES_ENABLED, jetpackFeaturesEnabled); fragment.setArguments(args); + SavedInstanceDatabase db = SavedInstanceDatabase.Companion.getDatabase(context); + if (db != null) { + db.addParcel(ARG_GUTENBERG_WEB_VIEW_AUTH_DATA, webViewAuthorizationData); + db.addParcel(ARG_GUTENBERG_PROPS_BUILDER, gutenbergPropsBuilder); + } return fragment; } @@ -199,12 +200,17 @@ public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getGutenbergContainerFragment() == null) { - GutenbergPropsBuilder gutenbergPropsBuilder = getArguments().getParcelable(ARG_GUTENBERG_PROPS_BUILDER); + GutenbergPropsBuilder gutenbergPropsBuilder = null; + SavedInstanceDatabase db = SavedInstanceDatabase.Companion.getDatabase(getContext()); + if (db != null) { + gutenbergPropsBuilder = db.getParcel(ARG_GUTENBERG_PROPS_BUILDER, GutenbergPropsBuilder.CREATOR); + } mCurrentGutenbergPropsBuilder = gutenbergPropsBuilder; FragmentManager fragmentManager = getChildFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); - GutenbergContainerFragment fragment = GutenbergContainerFragment.newInstance(gutenbergPropsBuilder); + GutenbergContainerFragment fragment = + GutenbergContainerFragment.newInstance(requireContext(), gutenbergPropsBuilder); fragment.setRetainInstance(true); fragmentTransaction.add(fragment, GutenbergContainerFragment.TAG); fragmentTransaction.commitNow(); @@ -645,9 +651,16 @@ private void initializeSavingProgressDialog() { } } + private GutenbergWebViewAuthorizationData getGutenbergWebViewAuthorizationData() { + SavedInstanceDatabase db = SavedInstanceDatabase.Companion.getDatabase(getContext()); + if (db != null) { + return db.getParcel(ARG_GUTENBERG_WEB_VIEW_AUTH_DATA, GutenbergWebViewAuthorizationData.CREATOR); + } + return null; + } + private void openGutenbergWebViewActivity(String content, String blockId, String blockName, String blockTitle) { - GutenbergWebViewAuthorizationData gutenbergWebViewAuthData = - getArguments().getParcelable(ARG_GUTENBERG_WEB_VIEW_AUTH_DATA); + GutenbergWebViewAuthorizationData gutenbergWebViewAuthData = getGutenbergWebViewAuthorizationData(); // There is a chance that isJetpackSsoEnabled has changed on the server // so we need to make sure that we have fresh value of it. @@ -727,8 +740,8 @@ private ArrayList initOtherMediaImageOptions() { } boolean jetpackFeaturesEnabled = arguments.getBoolean(ARG_JETPACK_FEATURES_ENABLED); - GutenbergWebViewAuthorizationData gutenbergWebViewAuthorizationData = - arguments.getParcelable(ARG_GUTENBERG_WEB_VIEW_AUTH_DATA); + GutenbergWebViewAuthorizationData gutenbergWebViewAuthorizationData = getGutenbergWebViewAuthorizationData(); + boolean supportStockPhotos = gutenbergWebViewAuthorizationData.isSiteUsingWPComRestAPI() && jetpackFeaturesEnabled; boolean supportsTenor = jetpackFeaturesEnabled; diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/savedinstance/ParcelableObject.kt b/libs/editor/src/main/java/org/wordpress/android/editor/savedinstance/ParcelableObject.kt new file mode 100644 index 000000000000..37b86e8e6555 --- /dev/null +++ b/libs/editor/src/main/java/org/wordpress/android/editor/savedinstance/ParcelableObject.kt @@ -0,0 +1,30 @@ +package org.wordpress.android.editor.savedinstance + +import android.os.Parcel +import android.os.Parcelable + +class ParcelableObject { + private val parcel = Parcel.obtain() + + constructor(parcelable: Parcelable) { + parcelable.writeToParcel(parcel, 0) + } + + constructor(data: ByteArray) { + parcel.unmarshall(data, 0, data.size) + parcel.setDataPosition(0) + } + + fun toBytes(): ByteArray { + return parcel.marshall() + } + + fun getParcel(): Parcel { + parcel.setDataPosition(0) + return parcel + } + + fun recycle() { + parcel.recycle() + } +} diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/savedinstance/SavedInstanceDatabase.kt b/libs/editor/src/main/java/org/wordpress/android/editor/savedinstance/SavedInstanceDatabase.kt new file mode 100644 index 000000000000..20192b816ef9 --- /dev/null +++ b/libs/editor/src/main/java/org/wordpress/android/editor/savedinstance/SavedInstanceDatabase.kt @@ -0,0 +1,84 @@ +package org.wordpress.android.editor.savedinstance + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.os.Parcelable +import org.wordpress.android.editor.savedinstance.SavedParcelTable.createTable +import org.wordpress.android.editor.savedinstance.SavedParcelTable.dropTable + +/** + * Database for the saved instance state data + */ +class SavedInstanceDatabase(context: Context?) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { + override fun onOpen(db: SQLiteDatabase) { + super.onOpen(db) + } + + override fun onCreate(db: SQLiteDatabase) { + createAllTables(db) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + reset(db) + } + + override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + reset(db) + } + + private fun createAllTables(db: SQLiteDatabase) { + createTable(db) + } + + private fun dropAllTables(db: SQLiteDatabase) { + dropTable(db) + } + + fun reset(db: SQLiteDatabase) { + db.beginTransaction() + try { + dropAllTables(db) + createAllTables(db) + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun addParcel(parcelId: String, parcel: Parcelable?) { + parcel?.let { + SavedParcelTable.addParcel(writableDatabase, parcelId, it) + } + } + + fun getParcel(parcelId: String, creator: Parcelable.Creator): T? { + return SavedParcelTable.getParcel(readableDatabase, parcelId, creator) + } + + fun hasParcel(parcelId: String): Boolean { + return SavedParcelTable.hasParcel(readableDatabase, parcelId) + } + + companion object { + private const val DB_NAME = "wpsavedinstance.db" + private const val DB_VERSION = 1 + + private var savedInstanceDb: SavedInstanceDatabase? = null + private val DB_LOCK = Any() + + fun getDatabase(context: Context): SavedInstanceDatabase? { + if (savedInstanceDb == null) { + synchronized(DB_LOCK) { + if (savedInstanceDb == null) { + savedInstanceDb = SavedInstanceDatabase(context.applicationContext) + // this ensures that onOpen() is called with a writable database + // (open will fail if app calls getReadableDb() first) + savedInstanceDb?.writableDatabase + } + } + } + return savedInstanceDb + } + } +} diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/savedinstance/SavedParcelTable.kt b/libs/editor/src/main/java/org/wordpress/android/editor/savedinstance/SavedParcelTable.kt new file mode 100644 index 000000000000..08c1e82e2cb9 --- /dev/null +++ b/libs/editor/src/main/java/org/wordpress/android/editor/savedinstance/SavedParcelTable.kt @@ -0,0 +1,62 @@ +package org.wordpress.android.editor.savedinstance + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import android.os.Parcelable +import org.wordpress.android.util.SqlUtils + +object SavedParcelTable { + private const val SAVED_PARCEL_TABLE = "tbl_saved_parcel" + private const val PARCEL_ID = "parcel_id" + private const val PARCEL_DATA = "parcel_data" + fun createTable(db: SQLiteDatabase) { + db.execSQL( + "CREATE TABLE " + SAVED_PARCEL_TABLE + " (" + + PARCEL_ID + " TEXT," + + PARCEL_DATA + " BLOB," + + " PRIMARY KEY (" + PARCEL_ID + ")" + + ")" + ) + } + + fun dropTable(db: SQLiteDatabase) { + db.execSQL("DROP TABLE IF EXISTS $SAVED_PARCEL_TABLE") + } + + fun reset(db: SQLiteDatabase) { + dropTable(db) + createTable(db) + } + + fun addParcel(writableDb: SQLiteDatabase?, parcelId: String, parcel: Parcelable) { + val parcelable = ParcelableObject(parcel) + val values = ContentValues() + values.put(PARCEL_ID, parcelId) + values.put(PARCEL_DATA, parcelable.toBytes()) + writableDb?.insertWithOnConflict(SAVED_PARCEL_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE) + parcelable.recycle() + } + + fun getParcel(readableDb: SQLiteDatabase?, parcelId: String, creator: Parcelable.Creator): T? { + val db = readableDb ?: return null + val c = db.rawQuery("SELECT * FROM $SAVED_PARCEL_TABLE WHERE $PARCEL_ID ='$parcelId'", null) + return try { + if (c.moveToFirst()) { + val parcelableObject = ParcelableObject(c.getBlob(c.getColumnIndexOrThrow(PARCEL_DATA))) + val parcelable = creator.createFromParcel(parcelableObject.getParcel()) + parcelableObject.recycle() + parcelable + } else { + null + } + } finally { + SqlUtils.closeCursor(c) + } + } + + fun hasParcel(readableDb: SQLiteDatabase?, parcelId: String): Boolean { + val db = readableDb ?: return false + val c = SqlUtils.intForQuery(db, "SELECT COUNT(*) FROM $SAVED_PARCEL_TABLE WHERE $PARCEL_ID ='$parcelId'", null) + return c > 0 + } +} diff --git a/settings.gradle b/settings.gradle index 22b25fc3c226..5ce20fe7d7ed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,6 +7,7 @@ pluginManagement { gradle.ext.daggerVersion = "2.46.1" gradle.ext.detektVersion = '1.23.0' gradle.ext.violationCommentsVersion = '1.67' + gradle.ext.measureBuildsVersion = '2.0.3' plugins { id "org.jetbrains.kotlin.android" version gradle.ext.kotlinVersion @@ -22,12 +23,14 @@ pluginManagement { id "io.sentry.android.gradle" version gradle.ext.sentryVersion id "io.gitlab.arturbosch.detekt" version gradle.ext.detektVersion id "se.bjurr.violations.violation-comments-to-github-gradle-plugin" version gradle.ext.violationCommentsVersion + id 'com.automattic.android.measure-builds' version gradle.ext.measureBuildsVersion } repositories { maven { url 'https://a8c-libs.s3.amazonaws.com/android' content { includeGroup "com.automattic.android" + includeGroup "com.automattic.android.measure-builds" } } gradlePluginPortal() diff --git a/version.properties b/version.properties index 55946b5faf3b..1a4544a6db99 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -versionName=23.8-rc-2 -versionCode=1388 \ No newline at end of file +versionName=23.8 +versionCode=1392 \ No newline at end of file