diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ec310f03d8f8..12fb366b7d8a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -32,7 +32,7 @@ Fixes # ----- -## Testing Checklist: +## Testing Checklist (strike-out the not-applying and unnecessary ones): - [ ] WordPress.com sites and self-hosted Jetpack sites. - [ ] Portrait and landscape orientations. diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 5915d2ffb837..5a2791307788 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,7 +2,9 @@ 24.5 ----- - +* [*] [internal] Block editor: Remove code associated to Story block [https://github.com/wordpress-mobile/WordPress-Android/pull/20400] +* [*] [Jetpack-only] Fixes broken links on some notifications [https://github.com/wordpress-mobile/WordPress-Android/pull/20417] +* [**] [internal] Block editor: Upgrade React Native to version 0.73.3 [#20167] 24.4 ----- diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 6329c75f8e74..810ee938ee9d 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -4,7 +4,6 @@ import se.bjurr.violations.lib.model.SEVERITY plugins { id "com.android.application" id "org.jetbrains.kotlin.android" - id "org.jetbrains.kotlin.kapt" id "org.jetbrains.kotlin.plugin.parcelize" id "org.jetbrains.kotlin.plugin.allopen" id "io.sentry.android.gradle" @@ -12,6 +11,7 @@ plugins { id "com.google.gms.google-services" id "com.google.dagger.hilt.android" id "org.jetbrains.kotlinx.kover" + id "com.google.devtools.ksp" } sentry { @@ -141,9 +141,9 @@ android { buildConfigField "boolean", "PLANS_IN_SITE_CREATION", "false" buildConfigField "boolean", "READER_IMPROVEMENTS", "false" buildConfigField "boolean", "BLOGANUARY_DASHBOARD_NUDGE", "false" - buildConfigField "boolean", "IN_APP_REVIEWS", "false" buildConfigField "boolean", "DYNAMIC_DASHBOARD_CARDS", "false" buildConfigField "boolean", "STATS_TRAFFIC_TAB", "false" + buildConfigField "boolean", "READER_DISCOVER_NEW_ENDPOINT", "false" // Override these constants in jetpack product flavor to enable/ disable features buildConfigField "boolean", "ENABLE_SITE_CREATION", "true" @@ -158,6 +158,7 @@ android { buildConfigField "boolean", "BLAZE_MANAGE_CAMPAIGNS", "false" buildConfigField "boolean", "DASHBOARD_PERSONALIZATION", "false" buildConfigField "boolean", "ENABLE_SITE_MONITORING", "false" + buildConfigField "boolean", "SYNC_PUBLISHING", "false" manifestPlaceholders = [magicLinkScheme:"wordpress"] } @@ -335,16 +336,12 @@ static def addBuildConfigFieldsFromPrefixedProperties(variant, properties, prefi variant.buildConfigField "String", it.key.toUpperCase(), "\"${it.value}\"" } } -kapt { - // Enable to infer error types in stubs (see: https://kotlinlang.org/docs/kapt.html#non-existent-type-correction) - correctErrorTypes true -} dependencies { implementation 'androidx.webkit:webkit:1.10.0' implementation "androidx.navigation:navigation-compose:$androidxComposeNavigationVersion" compileOnly project(path: ':libs:annotations') - kapt project(':libs:processors') + ksp project(':libs:processors') implementation (project(path:':libs:networking')) { exclude group: "com.android.volley" exclude group: 'org.wordpress', module: 'utils' @@ -446,7 +443,7 @@ dependencies { exclude group: 'androidx.appcompat', module: 'appcompat' } implementation "com.github.bumptech.glide:glide:$glideVersion" - kapt "com.github.bumptech.glide:compiler:$glideVersion" + ksp "com.github.bumptech.glide:ksp:$glideVersion" implementation "com.github.bumptech.glide:volley-integration:$glideVersion" implementation "com.github.indexos.media-for-mobile:domain:$indexosMediaForMobileVersion" implementation "com.github.indexos.media-for-mobile:android:$indexosMediaForMobileVersion" @@ -460,9 +457,9 @@ dependencies { exclude group: 'com.android.support', module: 'support-annotations' } implementation "com.google.dagger:dagger-android-support:$gradle.ext.daggerVersion" - kapt "com.google.dagger:dagger-android-processor:$gradle.ext.daggerVersion" + ksp "com.google.dagger:dagger-android-processor:$gradle.ext.daggerVersion" implementation "com.google.dagger:hilt-android:$gradle.ext.daggerVersion" - kapt "com.google.dagger:hilt-compiler:$gradle.ext.daggerVersion" + ksp "com.google.dagger:hilt-compiler:$gradle.ext.daggerVersion" testImplementation("androidx.arch.core:core-testing:$androidxArchCoreVersion", { exclude group: 'com.android.support', module: 'support-compat' @@ -517,7 +514,7 @@ dependencies { androidTestImplementation (name:'cloudtestingscreenshotter_lib', ext:'aar') // Screenshots on Firebase Cloud Testing androidTestImplementation "androidx.work:work-testing:$androidxWorkManagerVersion" androidTestImplementation "com.google.dagger:hilt-android-testing:$gradle.ext.daggerVersion" - kaptAndroidTest "com.google.dagger:hilt-android-compiler:$gradle.ext.daggerVersion" + kspAndroidTest "com.google.dagger:hilt-android-compiler:$gradle.ext.daggerVersion" // Enables Java 8+ API desugaring support coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$androidDesugarVersion" lintChecks "org.wordpress:lint:$wordPressLintVersion" diff --git a/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.java b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.java deleted file mode 100644 index d52f9ebab469..000000000000 --- a/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.wordpress.android.ui.notifications; - -import android.text.SpannableStringBuilder; - -import org.junit.Test; -import org.wordpress.android.ui.notifications.utils.NotificationsUtils; - -import static junit.framework.TestCase.assertFalse; -import static junit.framework.TestCase.assertTrue; - -import dagger.hilt.android.testing.HiltAndroidTest; - -@HiltAndroidTest -public class NotificationsUtilsTest { - @Test - public void testSpannableHasCharacterAtIndex() { - SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("This is only a test."); - - assertTrue(NotificationsUtils.spannableHasCharacterAtIndex(spannableStringBuilder, 's', 3)); - assertFalse(NotificationsUtils.spannableHasCharacterAtIndex(spannableStringBuilder, 's', 4)); - - // Test with bogus params - assertFalse(NotificationsUtils.spannableHasCharacterAtIndex(null, 'b', -1)); - } -} diff --git a/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.kt b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.kt new file mode 100644 index 000000000000..5dcc36f90040 --- /dev/null +++ b/WordPress/src/androidTest/java/org/wordpress/android/ui/notifications/NotificationsUtilsTest.kt @@ -0,0 +1,118 @@ +package org.wordpress.android.ui.notifications + +import android.text.SpannableStringBuilder +import android.text.style.ClickableSpan +import android.widget.TextView +import androidx.test.platform.app.InstrumentationRegistry +import dagger.hilt.android.testing.HiltAndroidTest +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import org.junit.Test +import org.wordpress.android.fluxc.tools.FormattableContent +import org.wordpress.android.fluxc.tools.FormattableRange +import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan +import org.wordpress.android.ui.notifications.utils.NotificationsUtils + +@HiltAndroidTest +class NotificationsUtilsTest { + @Test + fun testSpannableHasCharacterAtIndex() { + val spannableStringBuilder = SpannableStringBuilder("This is only a test.") + + assertTrue(NotificationsUtils.spannableHasCharacterAtIndex(spannableStringBuilder, 's', 3)) + assertFalse(NotificationsUtils.spannableHasCharacterAtIndex(spannableStringBuilder, 's', 4)) + + // Test with bogus params + assertFalse(NotificationsUtils.spannableHasCharacterAtIndex(null, 'b', -1)) + } + + @Test + fun testGetSpannableContentForRangesAndSkipInvalidUrls() { + // Create a FormattableContent object + val range1 = FormattableRange(indices = listOf(10, 14), url = "https://example.com", type = "a") + val range2 = FormattableRange(indices = listOf(5, 20), url = "", type = "a") // invalid url to skip + val formattableContent = FormattableContent( + text = "This is a test content with a link", + ranges = listOf(range1, range2) + ) + + // Create a TextView object + val textView = TextView(InstrumentationRegistry.getInstrumentation().context) + + // Call the method with the created objects + val result = NotificationsUtils.getSpannableContentForRanges(formattableContent, textView, false) {} + + // Check the result + assertNotNull(result) + assertEquals("This is a test content with a link", result.toString()) + + // Check if the link is correctly set + val spans = result.getSpans(10, 14, ClickableSpan::class.java) + assertTrue(spans.size == 1) + assertEquals("https://example.com", (spans[0] as NoteBlockClickableSpan).formattableRange.url) + } + + @Test + fun testGetSpannableContentForRangesWithNoRanges() { + // Create a FormattableContent object with no ranges + val formattableContent = FormattableContent(text = "This is a test content with no link") + + // Create a TextView object + val textView = TextView(InstrumentationRegistry.getInstrumentation().context) + + // Call the method with the created objects + val result = NotificationsUtils.getSpannableContentForRanges(formattableContent, textView, false) {} + + // Check the result + assertNotNull(result) + assertEquals("This is a test content with no link", result.toString()) + + // Check if no ClickableSpan is set + val spans = result.getSpans(0, result.length, ClickableSpan::class.java) + assertTrue(spans.isEmpty()) + } + + @Test + fun testGetSpannableContentForRangesWithInvalidIndex() { + // Create a FormattableContent object with a range with an invalid index + val range = FormattableRange(indices = listOf(50, 54), url = "https://example.com", type = "a") + val formattableContent = FormattableContent(text = "This is a test content", ranges = listOf(range)) + + // Create a TextView object + val textView = TextView(InstrumentationRegistry.getInstrumentation().context) + + // Call the method with the created objects + val result = NotificationsUtils.getSpannableContentForRanges(formattableContent, textView, false) {} + + // Check the result + assertNotNull(result) + assertEquals("This is a test content", result.toString()) + + // Check if no ClickableSpan is set + val spans = result.getSpans(0, result.length, ClickableSpan::class.java) + assertTrue(spans.isEmpty()) + } + + @Test + fun testGetSpannableContentForRangesWithNullUrl() { + // Create a FormattableContent object with a range with a null URL + val range = FormattableRange(indices = listOf(10, 14), url = null, type = "a") + val formattableContent = FormattableContent(text = "This is a test content with a link", ranges = listOf(range)) + + // Create a TextView object + val textView = TextView(InstrumentationRegistry.getInstrumentation().context) + + // Call the method with the created objects + val result = NotificationsUtils.getSpannableContentForRanges(formattableContent, textView, false) {} + + // Check the result + assertNotNull(result) + assertEquals("This is a test content with a link", result.toString()) + + // Check if no ClickableSpan is set for the range with the null URL + val spans = result.getSpans(10, 14, ClickableSpan::class.java) + assertTrue(spans.isEmpty()) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/modules/PostModule.kt b/WordPress/src/main/java/org/wordpress/android/modules/PostModule.kt new file mode 100644 index 000000000000..14d034b274b5 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/modules/PostModule.kt @@ -0,0 +1,17 @@ +package org.wordpress.android.modules + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.wordpress.android.ui.posts.IPostFreshnessChecker +import org.wordpress.android.ui.posts.PostFreshnessCheckerImpl +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +class PostModule { + @Singleton + @Provides + fun providePostFreshnessChecker(): IPostFreshnessChecker = PostFreshnessCheckerImpl() +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityNavigator.kt b/WordPress/src/main/java/org/wordpress/android/ui/ActivityNavigator.kt index 9378e0842f6f..f5a2ee3a87cf 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityNavigator.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityNavigator.kt @@ -49,7 +49,7 @@ class ActivityNavigator @Inject constructor() { fun navigateToCampaignDetailPage( context: Context, - campaignId: Int, + campaignId: String, campaignDetailPageSource: CampaignDetailPageSource ) { context.startActivity( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignPage.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignPage.kt index 58f41f95e207..bbd081cc570b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/BlazeCampaignPage.kt @@ -10,6 +10,6 @@ import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.CampaignLis @SuppressLint("ParcelCreator") sealed class BlazeCampaignPage : Parcelable { data class CampaignListingPage(val source: CampaignListingPageSource) : BlazeCampaignPage() - data class CampaignDetailsPage(val campaignId: Int, val source: CampaignDetailPageSource) : BlazeCampaignPage() + data class CampaignDetailsPage(val campaignId: String, val source: CampaignDetailPageSource) : BlazeCampaignPage() object Done: BlazeCampaignPage() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailFragment.kt index de7cc0bf6afa..5e5a81df9e06 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailFragment.kt @@ -54,10 +54,10 @@ private const val CAMPAIGN_DETAIL_CAMPAIGN_ID = "campaign_detail_campaign_id" @AndroidEntryPoint class CampaignDetailFragment : Fragment(), CampaignDetailWebViewClient.CampaignDetailWebViewClientListener { companion object { - fun newInstance(campaignId: Int, source: CampaignDetailPageSource) = CampaignDetailFragment().apply { + fun newInstance(campaignId: String, source: CampaignDetailPageSource) = CampaignDetailFragment().apply { arguments = Bundle().apply { putSerializable(CAMPAIGN_DETAIL_PAGE_SOURCE, source) - putInt(CAMPAIGN_DETAIL_CAMPAIGN_ID, campaignId) + putString(CAMPAIGN_DETAIL_CAMPAIGN_ID, campaignId) } } } @@ -110,7 +110,7 @@ class CampaignDetailFragment : Fragment(), CampaignDetailWebViewClient.CampaignD ?: CampaignDetailPageSource.UNKNOWN } - private fun getCampaignId() = requireArguments().getInt(CAMPAIGN_DETAIL_CAMPAIGN_ID) + private fun getCampaignId() = requireArguments().getString(CAMPAIGN_DETAIL_CAMPAIGN_ID) ?: "" override fun onRedirectToExternalBrowser(url: String) = viewModel.onRedirectToExternalBrowser(url) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailViewModel.kt index a7230da28acc..b50e151d2a81 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaigndetail/CampaignDetailViewModel.kt @@ -30,7 +30,7 @@ class CampaignDetailViewModel @Inject constructor( @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher ) : ScopedViewModel(bgDispatcher) { private lateinit var pageSource: CampaignDetailPageSource - private var campaignId: Int = 0 + private var campaignId: String = "" private val _actionEvents = Channel(Channel.BUFFERED) val actionEvents = _actionEvents.receiveAsFlow() @@ -38,7 +38,7 @@ class CampaignDetailViewModel @Inject constructor( private val _uiState = MutableStateFlow(CampaignDetailUiState.Preparing) val uiState = _uiState as StateFlow - fun start(campaignId: Int, campaignDetailPageSource: CampaignDetailPageSource) { + fun start(campaignId: String, campaignDetailPageSource: CampaignDetailPageSource) { this.campaignId = campaignId this.pageSource = campaignDetailPageSource @@ -76,7 +76,7 @@ class CampaignDetailViewModel @Inject constructor( pathComponents = arrayOf( ADVERTISING_PATH, CAMPAIGNS_PATH, - campaignId.toString(), + campaignId, extractAndSanitizeSiteUrl() ), source = pageSource.trackingName diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListUseCases.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListUseCases.kt index 18b43200e49b..bc5502271a59 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListUseCases.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListUseCases.kt @@ -9,13 +9,26 @@ class FetchCampaignListUseCase @Inject constructor( private val store: BlazeCampaignsStore, private val mapper: CampaignListingUIModelMapper ) { + companion object { + const val PAGE_SIZE = 10 + } + @Suppress("ReturnCount") - suspend fun execute(site: SiteModel, page: Int): Result> { - val result = store.fetchBlazeCampaigns(site, page) - if (result.isError || result.model == null) return Result.Failure(GenericError) + suspend fun execute( + site: SiteModel, + offset: Int, + pageSize: Int = PAGE_SIZE + ): Result { + val result = store.fetchBlazeCampaigns(site = site, offset = offset, perPage = pageSize) + if (result.isError || result.model == null) return Result.Failure(GenericResult) val campaigns = result.model!!.campaigns if (campaigns.isEmpty()) return Result.Failure(NoCampaigns) - return Result.Success(mapper.mapToCampaignModels(campaigns)) + return Result.Success( + FetchedCampaignsResult( + campaigns = mapper.mapToCampaignModels(campaigns), + totalItems = result.model!!.totalItems + ) + ) } } @@ -24,14 +37,19 @@ class GetCampaignListFromDbUseCase @Inject constructor( private val mapper: CampaignListingUIModelMapper ) { suspend fun execute(site: SiteModel): Result> { - val result = store.getBlazeCampaigns(site) - if (result.campaigns.isEmpty()) return Result.Failure(NoCampaigns) - return Result.Success(mapper.mapToCampaignModels(result.campaigns)) + val campaigns = store.getBlazeCampaigns(site) + if (campaigns.isEmpty()) return Result.Failure(NoCampaigns) + return Result.Success(mapper.mapToCampaignModels(campaigns)) } } -sealed interface NetworkError +data class FetchedCampaignsResult( + val campaigns: List, + val totalItems: Int +) + +sealed interface NetworkResult -object GenericError : NetworkError +object GenericResult : NetworkResult -object NoCampaigns : NetworkError +object NoCampaigns : NetworkResult diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapper.kt index e0543e72162f..920c8f180de7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapper.kt @@ -6,8 +6,7 @@ import org.wordpress.android.ui.stats.refresh.utils.ONE_THOUSAND import org.wordpress.android.ui.stats.refresh.utils.StatsUtils import org.wordpress.android.ui.utils.UiString import javax.inject.Inject - -const val CENTS_IN_DOLLARS = 100 +import kotlin.math.roundToInt class CampaignListingUIModelMapper @Inject constructor( private val statsUtils: StatsUtils @@ -24,7 +23,7 @@ class CampaignListingUIModelMapper @Inject constructor( featureImageUrl = campaignModel.imageUrl, impressions = mapToStatsStringIfNeeded(campaignModel.impressions), clicks = mapToStatsStringIfNeeded(campaignModel.clicks), - budget = convertToDollars(campaignModel.budgetCents) + budget = UiString.UiStringText("$${campaignModel.totalBudget.roundToInt()}") ) } @@ -36,8 +35,4 @@ class CampaignListingUIModelMapper @Inject constructor( null } } - - private fun convertToDollars(budgetCents: Long): UiString { - return UiString.UiStringText("$" + (budgetCents / CENTS_IN_DOLLARS).toString()) - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingViewModel.kt index 182a29884a17..b92dbd672800 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingViewModel.kt @@ -50,8 +50,7 @@ class CampaignListingViewModel @Inject constructor( private val _onSelectedSiteMissing = MutableLiveData() val onSelectedSiteMissing = _onSelectedSiteMissing as LiveData - private var page = 1 - private var limitPerPage: Int = 10 + private var offset = 0 private var isLastPage: Boolean = false fun start(campaignListingPageSource: CampaignListingPageSource) { @@ -79,11 +78,11 @@ class CampaignListingViewModel @Inject constructor( if (networkUtilsWrapper.isNetworkAvailable().not()) { showNoNetworkError() } else { - when (val campaignResult = fetchCampaignListUseCase.execute(site, page)) { - is Result.Success -> showCampaigns(campaignResult.value) + when (val campaignResult = fetchCampaignListUseCase.execute(site, offset)) { + is Result.Success -> showCampaigns(campaignResult.value.campaigns) is Result.Failure -> { when (campaignResult.value) { - is GenericError -> showGenericError() + is GenericResult -> showGenericError() is NoCampaigns -> showNoCampaigns() } } @@ -119,7 +118,6 @@ class CampaignListingViewModel @Inject constructor( (_uiState.value as CampaignListingUiState.Success).pagingDetails.loadingNext.not() && isLastPage.not() ) { - page++ showLoadingMore() fetchMoreCampaigns() } @@ -131,16 +129,18 @@ class CampaignListingViewModel @Inject constructor( disableLoadingMore() showSnackBar(R.string.campaign_listing_page_error_refresh_no_network_available) } else { - when (val campaignResult = fetchCampaignListUseCase.execute(site, page)) { + when (val campaignResult = fetchCampaignListUseCase.execute(site, offset)) { is Result.Success -> { val currentUiState = _uiState.value as CampaignListingUiState.Success - isLastPage = campaignResult.value.isEmpty() || campaignResult.value.size < limitPerPage - showCampaigns(currentUiState.campaigns + campaignResult.value) + val allCampaigns = currentUiState.campaigns + campaignResult.value.campaigns + isLastPage = allCampaigns.size >= campaignResult.value.totalItems + offset += allCampaigns.size + showCampaigns(allCampaigns) } is Result.Failure -> { when (campaignResult.value) { - is GenericError -> { + is GenericResult -> { disableLoadingMore() showSnackBar(R.string.campaign_listing_page_error_refresh_could_not_fetch_campaigns) } @@ -168,7 +168,7 @@ class CampaignListingViewModel @Inject constructor( } private fun onCampaignClicked(campaignModel: CampaignModel) { - _navigation.postValue(Event(CampaignListingNavigation.CampaignDetailPage(campaignModel.id.toInt()))) + _navigation.postValue(Event(CampaignListingNavigation.CampaignDetailPage(campaignModel.id))) } private fun showNoCampaigns() { @@ -180,23 +180,23 @@ class CampaignListingViewModel @Inject constructor( } fun refreshCampaigns() { - page = 1 launch { _refresh.postValue(true) if (!networkUtilsWrapper.isNetworkAvailable()) { _refresh.postValue(false) showSnackBar(R.string.campaign_listing_page_error_refresh_no_network_available) } else { - when (val campaignResult = fetchCampaignListUseCase.execute(site, page)) { + offset = 0 + when (val campaignResult = fetchCampaignListUseCase.execute(site, offset)) { is Result.Success -> { _refresh.postValue(false) isLastPage = false - showCampaigns(campaignResult.value) + showCampaigns(campaignResult.value.campaigns) } is Result.Failure -> { when (campaignResult.value) { - is GenericError -> { + is GenericResult -> { _refresh.postValue(false) showSnackBar(R.string.campaign_listing_page_error_refresh_could_not_fetch_campaigns) } @@ -223,7 +223,7 @@ enum class CampaignListingPageSource(val trackingName: String) { sealed class CampaignListingNavigation { data class CampaignDetailPage( - val campaignId: Int, + val campaignId: String, val campaignDetailPageSource: CampaignDetailPageSource = CampaignDetailPageSource.CAMPAIGN_LISTING_PAGE ) : CampaignListingNavigation() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSlice.kt index 2686d691ecb7..8b118c4bd1e8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSlice.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSlice.kt @@ -155,7 +155,7 @@ class BlazeCardViewModelSlice @Inject constructor( Event(SiteNavigationAction.OpenPromoteWithBlazeOverlay(source = BlazeFlowSource.DASHBOARD_CARD)) } - private fun onCampaignClick(campaignId: Int) { + private fun onCampaignClick(campaignId: String) { _onNavigation.value = Event(SiteNavigationAction.OpenCampaignDetailPage(campaignId, CampaignDetailPageSource.DASHBOARD_CARD)) } 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 9c5f226eda9c..60dfe33e0b65 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 @@ -326,12 +326,12 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte val moreMenuOptions: MoreMenuOptions ) : BlazeCard(type = Type.BLAZE_CAMPAIGNS_CARD) { data class BlazeCampaignsCardItem( - val id: Int, + val id: String, val title: UiString, val status: CampaignStatus?, val featuredImageUrl: String?, val stats: BlazeCampaignStats?, - val onClick: (campaignId: Int) -> Unit, + val onClick: (campaignId: String) -> Unit, ) { data class BlazeCampaignStats( val impressions: UiString, 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 f5b256074321..d84d8b574504 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 @@ -188,7 +188,7 @@ sealed class MySiteCardAndItemBuilderParams { data class CampaignWithBlazeCardBuilderParams( val campaign: BlazeCampaignModel, val onCreateCampaignClick: () -> Unit, - val onCampaignClick: (campaignId: Int) -> Unit, + val onCampaignClick: (campaignId: String) -> Unit, val onCardClick: () -> Unit, val moreMenuParams: MoreMenuParams ) : BlazeCardBuilderParams() { 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 422a82a81a4c..698131c54497 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 @@ -84,7 +84,7 @@ sealed class SiteNavigationAction { data class OpenCampaignListingPage(val campaignListingPageSource: CampaignListingPageSource) : SiteNavigationAction() - data class OpenCampaignDetailPage(val campaignId: Int, val campaignDetailPageSource: CampaignDetailPageSource) : + data class OpenCampaignDetailPage(val campaignId: String, val campaignDetailPageSource: CampaignDetailPageSource) : SiteNavigationAction() object OpenDashboardPersonalization : SiteNavigationAction() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/blaze/BlazeCardSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/blaze/BlazeCardSource.kt index f5f87c2fe689..fcfb2c1c20f0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/blaze/BlazeCardSource.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/blaze/BlazeCardSource.kt @@ -56,7 +56,7 @@ class BlazeCardSource @Inject constructor( if (networkUtilsWrapper.isNetworkAvailable().not()) { getMostRecentCampaignFromDb(site) } else { - when (fetchCampaignListUseCase.execute(site = site, page = 1)) { + when (fetchCampaignListUseCase.execute(site = site, offset = 0)) { is Result.Success -> getMostRecentCampaignFromDb(site) // there are no campaigns or if there is an error , show blaze promo card is Result.Failure -> showPromoteWithBlazeCard() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/blaze/CampaignStatus.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/blaze/CampaignStatus.kt index b5f3cca85278..651a877063ba 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/blaze/CampaignStatus.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/blaze/CampaignStatus.kt @@ -8,12 +8,12 @@ import org.wordpress.android.util.AppLog @Suppress("MagicNumber") enum class CampaignStatus(val status: String, @StringRes val stringResource: Int) { + InModeration("pending", R.string.campaign_status_in_moderation), + Scheduled("scheduled", R.string.campaign_status_scheduled), Active("active", R.string.campaign_status_active), - Completed("finished", R.string.campaign_status_completed), Rejected("rejected", R.string.campaign_status_rejected), Canceled("canceled", R.string.campaign_status_canceled), - Scheduled("scheduled", R.string.campaign_status_scheduled), - InModeration("created", R.string.campaign_status_in_moderation); + Completed("finished", R.string.campaign_status_completed); companion object { fun fromString(status: String): CampaignStatus? { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java index 151916d79294..5653112f7af5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsUtils.java @@ -17,6 +17,7 @@ import android.view.View; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.app.NotificationManagerCompat; @@ -37,6 +38,7 @@ import org.wordpress.android.fluxc.tools.FormattableContentMapper; import org.wordpress.android.fluxc.tools.FormattableMedia; import org.wordpress.android.fluxc.tools.FormattableRange; +import org.wordpress.android.fluxc.tools.FormattableRangeType; import org.wordpress.android.models.Note; import org.wordpress.android.push.GCMMessageService; import org.wordpress.android.ui.notifications.blocks.NoteBlock; @@ -215,16 +217,17 @@ static SpannableStringBuilder getSpannableContentForRanges(FormattableContent fo * @param isFooter - Set if spannable should apply special formatting * @return Spannable string with formatted content */ - static SpannableStringBuilder getSpannableContentForRanges(FormattableContent formattableContent, - TextView textView, - final Function1 clickHandler, - boolean isFooter) { + @NonNull + static SpannableStringBuilder getSpannableContentForRanges( + @Nullable FormattableContent formattableContent, + @Nullable TextView textView, + @Nullable final Function1 clickHandler, + boolean isFooter + ) { Function1 clickListener = - clickHandler != null ? new Function1() { - @Override public Unit invoke(NoteBlockClickableSpan noteBlockClickableSpan) { - clickHandler.invoke(noteBlockClickableSpan.getFormattableRange()); - return null; - } + clickHandler != null ? noteBlockClickableSpan -> { + clickHandler.invoke(noteBlockClickableSpan.getFormattableRange()); + return null; } : null; return getSpannableContentForRanges(formattableContent, textView, @@ -241,11 +244,14 @@ static SpannableStringBuilder getSpannableContentForRanges(FormattableContent fo * @param isFooter - Set if spannable should apply special formatting * @return Spannable string with formatted content */ - private static SpannableStringBuilder getSpannableContentForRanges(FormattableContent formattableContent, - TextView textView, - boolean isFooter, - final Function1 - onNoteBlockTextClickListener) { + @NonNull + public static SpannableStringBuilder getSpannableContentForRanges( + @Nullable FormattableContent formattableContent, + @Nullable TextView textView, + boolean isFooter, + @Nullable final Function1 + onNoteBlockTextClickListener + ) { if (formattableContent == null) { return new SpannableStringBuilder(); } @@ -262,6 +268,9 @@ private static SpannableStringBuilder getSpannableContentForRanges(FormattableCo List rangesArray = formattableContent.getRanges(); if (rangesArray != null) { for (FormattableRange range : rangesArray) { + // Skip ranges with UNKNOWN type and no URL since they are not actionable + if (range.rangeType() == FormattableRangeType.UNKNOWN && TextUtils.isEmpty(range.getUrl())) continue; + NoteBlockClickableSpan clickableSpan = new NoteBlockClickableSpan(range, shouldLink, isFooter) { @Override 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 20d861c576fd..f3802a04d743 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 @@ -17,6 +17,7 @@ import android.view.View; import android.view.ViewGroup; import android.webkit.MimeTypeMap; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.Toast; @@ -221,6 +222,7 @@ import org.wordpress.android.util.analytics.AnalyticsUtils.BlockEditorEnabledSource; import org.wordpress.android.util.config.ContactSupportFeatureConfig; import org.wordpress.android.util.config.GlobalStyleSupportFeatureConfig; +import org.wordpress.android.util.config.SyncPublishingFeatureConfig; import org.wordpress.android.util.extensions.AppBarLayoutExtensionsKt; import org.wordpress.android.util.helpers.MediaFile; import org.wordpress.android.util.helpers.MediaGallery; @@ -382,6 +384,8 @@ enum RestartEditorOptions { private boolean mHtmlModeMenuStateOn = false; + private FrameLayout mUpdatingPostArea; + @Inject Dispatcher mDispatcher; @Inject AccountStore mAccountStore; @Inject SiteStore mSiteStore; @@ -423,6 +427,7 @@ enum RestartEditorOptions { @Inject BloggingPromptsStore mBloggingPromptsStore; @Inject JetpackFeatureRemovalPhaseHelper mJetpackFeatureRemovalPhaseHelper; @Inject ContactSupportFeatureConfig mContactSupportFeatureConfig; + @Inject SyncPublishingFeatureConfig mSyncPublishingFeatureConfig; private StorePostViewModel mViewModel; private StorageUtilsViewModel mStorageUtilsViewModel; @@ -437,6 +442,11 @@ enum RestartEditorOptions { private ActivityResultLauncher mEditShareMessageActivityResultLauncher; + private final Handler mHideUpdatingPostAreaHandler = new Handler(Looper.getMainLooper()); + private Runnable mHideUpdatingPostAreaRunnable; + private long mUpdatingPostStartTime = 0L; + private static final long MIN_UPDATING_POST_DISPLAY_TIME = 2000L; // Minimum display time in milliseconds + public static boolean checkToRestart(@NonNull Intent data) { return data.hasExtra(EXTRA_RESTART_EDITOR) && RestartEditorOptions.valueOf(data.getStringExtra(EXTRA_RESTART_EDITOR)) @@ -558,12 +568,15 @@ public void handleOnBackPressed() { getOnBackPressedDispatcher().addCallback(this, callback); mDispatcher.register(this); + + // initialise ViewModels mViewModel = new ViewModelProvider(this, mViewModelFactory).get(StorePostViewModel.class); mStorageUtilsViewModel = new ViewModelProvider(this, mViewModelFactory).get(StorageUtilsViewModel.class); mEditorBloggingPromptsViewModel = new ViewModelProvider(this, mViewModelFactory) .get(EditorBloggingPromptsViewModel.class); mEditorJetpackSocialViewModel = new ViewModelProvider(this, mViewModelFactory) .get(EditorJetpackSocialViewModel.class); + setContentView(R.layout.new_edit_post_activity); createEditShareMessageActivityResultLauncher(); @@ -615,7 +628,6 @@ public void handleOnBackPressed() { mToolbar = findViewById(R.id.toolbar_main); setSupportActionBar(mToolbar); - final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); @@ -797,6 +809,46 @@ public void handleOnBackPressed() { mEditorJetpackSocialViewModel.start(mSite, mEditPostRepository); customizeToolbar(); + + mUpdatingPostArea = findViewById(R.id.updating); + + // check if post content needs updating + if (mSyncPublishingFeatureConfig.isEnabled()) { + mViewModel.checkIfUpdatedPostVersionExists(mEditPostRepository, mSite); + } + } + private void showUpdatingPostArea() { + mUpdatingPostArea.setVisibility(View.VISIBLE); + mUpdatingPostStartTime = System.currentTimeMillis(); + // Cancel any pending hide operations to avoid conflicts + if (mHideUpdatingPostAreaRunnable != null) { + mHideUpdatingPostAreaHandler.removeCallbacks(mHideUpdatingPostAreaRunnable); + } + } + + private void hideUpdatingPostArea() { + long elapsedTime = System.currentTimeMillis() - mUpdatingPostStartTime; + long delay = MIN_UPDATING_POST_DISPLAY_TIME - elapsedTime; + + if (delay > 0) { + // Delay hiding the view if the elapsed time is less than the minimum display time + hideUpdatingPostAreaWithDelay(delay); + } else { + // Hide the view immediately if the minimum display time has been met or exceeded + mUpdatingPostArea.setVisibility(View.GONE); + } + } + + private void hideUpdatingPostAreaWithDelay(long delay) { + // Define the runnable only once or ensure it's the same instance if it's already defined + if (mHideUpdatingPostAreaRunnable == null) { + mHideUpdatingPostAreaRunnable = () -> { + if (mUpdatingPostArea != null) { + mUpdatingPostArea.setVisibility(View.GONE); + } + }; + } + mHideUpdatingPostAreaHandler.postDelayed(mHideUpdatingPostAreaRunnable, delay); } private void customizeToolbar() { @@ -1013,6 +1065,27 @@ private void startObserving() { ); } }); + + mViewModel.getOnPostUpdateUiVisible().observe(this, isVisible -> { + if (isVisible) { + showUpdatingPostArea(); + } else { + hideUpdatingPostArea(); + } + }); + + mViewModel.getOnPostUpdateResult().observe(this, isSuccess -> { + if (isSuccess) { + mEditPostRepository.loadPostByLocalPostId(mEditPostRepository.getId()); + refreshEditorContent(); + } else { + ToastUtils.showToast( + EditPostActivity.this, + getString(R.string.editor_updating_post_failed), + ToastUtils.Duration.SHORT + ); + } + }); } private void initializePostObject() { @@ -1094,6 +1167,10 @@ protected void onPause() { if (mShowPrepublishingBottomSheetHandler != null && mShowPrepublishingBottomSheetRunnable != null) { mShowPrepublishingBottomSheetHandler.removeCallbacks(mShowPrepublishingBottomSheetRunnable); } + + if (mHideUpdatingPostAreaHandler != null && mHideUpdatingPostAreaRunnable != null) { + mHideUpdatingPostAreaHandler.removeCallbacks(mHideUpdatingPostAreaRunnable); + } } @Override diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/IPostFreshnessChecker.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/IPostFreshnessChecker.kt new file mode 100644 index 000000000000..0b97fd520f47 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/IPostFreshnessChecker.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.ui.posts + +import org.wordpress.android.fluxc.model.PostImmutableModel + +/** + * This interface is implemented by a component that determines if a post + * is "fresh" or we need to refetch it from the backend. + */ +interface IPostFreshnessChecker { + fun shouldRefreshPost(post: PostImmutableModel): Boolean +} + +interface TimeProvider { + fun currentTimeMillis(): Long +} + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostFreshnessCheckerImpl.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostFreshnessCheckerImpl.kt new file mode 100644 index 000000000000..26f1643b1770 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostFreshnessCheckerImpl.kt @@ -0,0 +1,25 @@ +package org.wordpress.android.ui.posts + +import org.wordpress.android.fluxc.model.PostImmutableModel + +class PostFreshnessCheckerImpl( + private val timeProvider: TimeProvider = SystemTimeProvider() +) : IPostFreshnessChecker { + override fun shouldRefreshPost(post: PostImmutableModel): Boolean { + return postNeedsRefresh(post) + } + + private fun postNeedsRefresh(post: PostImmutableModel) : Boolean { + return timeProvider.currentTimeMillis() - post.dbTimestamp > CACHE_VALIDITY_MILLIS + } + + companion object { + // Todo turn this into a remote config value + const val CACHE_VALIDITY_MILLIS = 20000 + } +} + +class SystemTimeProvider : TimeProvider { + override fun currentTimeMillis(): Long = System.currentTimeMillis() +} + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtilsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtilsWrapper.kt index 8c622c138a56..9e62b2c8a917 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtilsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtilsWrapper.kt @@ -16,7 +16,10 @@ import javax.inject.Inject * */ @Reusable -class PostUtilsWrapper @Inject constructor(private val dateProvider: DateProvider) { +class PostUtilsWrapper +@Inject constructor( + private val dateProvider: DateProvider +) { fun isPublishable(post: PostImmutableModel) = PostUtils.isPublishable(post) fun isPostInConflictWithRemote(post: PostImmutableModel) = diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StorePostViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StorePostViewModel.kt index 5aa35762de68..684285199203 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StorePostViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StorePostViewModel.kt @@ -12,15 +12,19 @@ import org.wordpress.android.editor.gutenberg.DialogVisibility.Hidden import org.wordpress.android.editor.gutenberg.DialogVisibility.Showing import org.wordpress.android.editor.gutenberg.DialogVisibilityProvider import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.generated.PostActionBuilder +import org.wordpress.android.fluxc.model.CauseOfOnPostChanged import org.wordpress.android.fluxc.model.PostImmutableModel import org.wordpress.android.fluxc.model.PostModel import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.PostStore.OnPostChanged import org.wordpress.android.fluxc.store.PostStore.OnPostUploaded +import org.wordpress.android.fluxc.store.PostStore.RemotePostPayload import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.posts.EditPostRepository import org.wordpress.android.ui.posts.EditPostRepository.UpdatePostResult +import org.wordpress.android.ui.posts.IPostFreshnessChecker import org.wordpress.android.ui.posts.PostUtilsWrapper import org.wordpress.android.ui.posts.SavePostToDbUseCase import org.wordpress.android.ui.posts.editor.StorePostViewModel.ActivityFinishState.SAVED_LOCALLY @@ -46,7 +50,8 @@ class StorePostViewModel private val uploadService: UploadServiceFacade, private val savePostToDbUseCase: SavePostToDbUseCase, private val networkUtils: NetworkUtilsWrapper, - private val dispatcher: Dispatcher + private val dispatcher: Dispatcher, + private val postFreshnessChecker: IPostFreshnessChecker ) : ScopedViewModel(uiCoroutineDispatcher), DialogVisibilityProvider { private var debounceCounter = 0 private var saveJob: Job? = null @@ -64,6 +69,12 @@ class StorePostViewModel } override val savingInProgressDialogVisibility: LiveData = _savingProgressDialogVisibility + private val _onPostUpdateUiVisible = MutableLiveData() + val onPostUpdateUiVisible: LiveData = _onPostUpdateUiVisible + + private val _onPostUpdateResult = MutableLiveData() + val onPostUpdateResult: LiveData = _onPostUpdateResult + init { dispatcher.register(this) } @@ -190,6 +201,21 @@ class StorePostViewModel _onFinish.postValue(Event(state)) } + fun checkIfUpdatedPostVersionExists( + editPostRepository: EditPostRepository, + site: SiteModel + ) { + editPostRepository.getPost()?.let { postModel -> + if (!postModel.isLocalDraft + && !postModel.isLocallyChanged + && postFreshnessChecker.shouldRefreshPost(postModel)) { + _onPostUpdateUiVisible.postValue(true) + val payload = RemotePostPayload(editPostRepository.getEditablePost(), site) + dispatcher.dispatch(PostActionBuilder.newFetchPostAction(payload)) + } + } + } + @Suppress("unused", "UNUSED_PARAMETER") @Subscribe fun onPostUploaded(event: OnPostUploaded) { @@ -200,6 +226,17 @@ class StorePostViewModel @Subscribe fun onPostChanged(event: OnPostChanged) { hideSavingProgressDialog() + + // Refresh post content if needed + (event.causeOfChange as? CauseOfOnPostChanged.UpdatePost)?.let { updatePost -> + // if post update is only local do nothing + if (!updatePost.isLocalUpdate) { + // Post the result based on `event.isError` + _onPostUpdateResult.postValue(!event.isError) + // Hide updating post area + _onPostUpdateUiVisible.postValue(false) + } + } } sealed class UpdateResult { 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 ccb36a80e3d1..7addc883bd7b 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 @@ -73,6 +73,8 @@ public enum DeletablePrefKey implements PrefKey { READER_ANALYTICS_COUNT_TAGS_TIMESTAMP, + READER_ANALYTICS_COUNT_SITES_TIMESTAMP, + // currently active tab on the main Reader screen when the user is in Reader READER_ACTIVE_TAB, @@ -1169,6 +1171,14 @@ public static void setReaderAnalyticsCountTagsTimestamp(long timestamp) { setLong(DeletablePrefKey.READER_ANALYTICS_COUNT_TAGS_TIMESTAMP, timestamp); } + public static long getReaderAnalyticsCountSitesTimestamp() { + return getLong(DeletablePrefKey.READER_ANALYTICS_COUNT_SITES_TIMESTAMP, -1); + } + + public static void setReaderAnalyticsCountSitesTimestamp(long timestamp) { + setLong(DeletablePrefKey.READER_ANALYTICS_COUNT_SITES_TIMESTAMP, timestamp); + } + public static long getReaderCssUpdatedTimestamp() { return getLong(DeletablePrefKey.READER_CSS_UPDATED_TIMESTAMP, 0); } 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 36f413574668..d8551ec3540e 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 @@ -63,6 +63,10 @@ class AppPrefsWrapper @Inject constructor() { get() = AppPrefs.getReaderTagsUpdatedTimestamp() set(timestamp) = AppPrefs.setReaderTagsUpdatedTimestamp(timestamp) + var readerAnalyticsCountTagsTimestamp: Long + get() = AppPrefs.getReaderAnalyticsCountTagsTimestamp() + set(timestamp) = AppPrefs.setReaderAnalyticsCountTagsTimestamp(timestamp) + var readerCssUpdatedTimestamp: Long get() = AppPrefs.getReaderCssUpdatedTimestamp() set(timestamp) = AppPrefs.setReaderCssUpdatedTimestamp(timestamp) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java index 2426057c20dc..41e5e1254acf 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java @@ -29,15 +29,18 @@ private ReaderEvents() { public static class FollowedTagsFetched { private final boolean mDidSucceed; private final boolean mDidChange; + private final int mTotalTags; - public FollowedTagsFetched(boolean didSucceed) { + public FollowedTagsFetched(boolean didSucceed, int tagsFollowed) { mDidSucceed = didSucceed; mDidChange = true; + mTotalTags = tagsFollowed; } - public FollowedTagsFetched(boolean didSucceed, boolean didChange) { + public FollowedTagsFetched(boolean didSucceed, int tagsFollowed, boolean didChange) { mDidSucceed = didSucceed; mDidChange = didChange; + mTotalTags = tagsFollowed; } public boolean didSucceed() { @@ -47,6 +50,9 @@ public boolean didSucceed() { public boolean didChange() { return mDidChange; } + public int getTotalTags() { + return mTotalTags; + } } public static class TagAdded { @@ -61,13 +67,19 @@ public String getTagName() { } } - public static class FollowedBlogsChanged { + public static class FollowedBlogsFetched { private final int mTotalSubscriptions; + private final boolean mDidChange; + + public boolean didChange() { + return mDidChange; + } public int getTotalSubscriptions() { return mTotalSubscriptions; } - public FollowedBlogsChanged(int totalSubscriptions) { + public FollowedBlogsFetched(int totalSubscriptions, boolean didChange) { mTotalSubscriptions = totalSubscriptions; + mDidChange = didChange; } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index fcad46bae9e5..809ec2bfcaa3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -84,6 +84,7 @@ import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredBottomSheetFragment; import org.wordpress.android.ui.pages.SnackbarMessageHolder; import org.wordpress.android.ui.prefs.AppPrefs; +import org.wordpress.android.ui.reader.ReaderEvents.FollowedBlogsFetched; import org.wordpress.android.ui.reader.ReaderEvents.FollowedTagsFetched; import org.wordpress.android.ui.reader.ReaderEvents.TagAdded; import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType; @@ -118,7 +119,6 @@ import org.wordpress.android.ui.reader.subfilter.SubfilterListItem.SiteAll; import org.wordpress.android.ui.reader.tracker.ReaderTracker; import org.wordpress.android.ui.reader.usecases.ReaderSiteFollowUseCase.FollowSiteState.FollowStatusChanged; -import org.wordpress.android.ui.reader.utils.DateProvider; import org.wordpress.android.ui.reader.utils.ReaderUtils; import org.wordpress.android.ui.reader.viewmodels.ReaderModeInfo; import org.wordpress.android.ui.reader.viewmodels.ReaderPostListViewModel; @@ -949,28 +949,18 @@ public void onEventMainThread(FollowedTagsFetched event) { updateCurrentTag(); } } - - // Check last time we've bumped tags followed analytics for this user, - // and bumping again if > 1 hrs - long tagsUpdatedTimestamp = AppPrefs.getReaderAnalyticsCountTagsTimestamp(); - long now = new DateProvider().getCurrentDate().getTime(); - if (now - tagsUpdatedTimestamp > 1000 * 60 * 60) { // 1 hr - ReaderTracker.trackFollowedTagsCount(ReaderTagTable.getFollowedTags().size()); - AppPrefs.setReaderAnalyticsCountTagsTimestamp(now); - } } @SuppressWarnings("unused") @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(ReaderEvents.FollowedBlogsChanged event) { + public void onEventMainThread(FollowedBlogsFetched event) { // refresh posts if user is viewing "Followed Sites" - if (getPostListType() == ReaderPostListType.TAG_FOLLOWED + if (event.didChange() + && getPostListType() == ReaderPostListType.TAG_FOLLOWED && hasCurrentTag() && (getCurrentTag().isFollowedSites() || getCurrentTag().isDefaultInMemoryTag())) { refreshPosts(); } - - ReaderTracker.trackSubscribedSitesCount(event.getTotalSubscriptions()); } @Override diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java index 3e8914829792..2bf6b1cb6b59 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java @@ -41,6 +41,7 @@ import org.wordpress.android.ui.LocaleAwareActivity; import org.wordpress.android.ui.RequestCodes; import org.wordpress.android.ui.prefs.AppPrefs; +import org.wordpress.android.ui.reader.ReaderEvents.FollowedBlogsFetched; import org.wordpress.android.ui.reader.ReaderEvents.FollowedTagsFetched; import org.wordpress.android.ui.reader.actions.ReaderActions; import org.wordpress.android.ui.reader.actions.ReaderBlogActions; @@ -211,9 +212,11 @@ public void onEventMainThread(FollowedTagsFetched event) { @SuppressWarnings("unused") @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(ReaderEvents.FollowedBlogsChanged event) { - AppLog.d(AppLog.T.READER, "reader subs > followed blogs changed"); - getPageAdapter().refreshBlogFragments(ReaderBlogType.FOLLOWED); + public void onEventMainThread(FollowedBlogsFetched event) { + if (event.didChange()) { + AppLog.d(AppLog.T.READER, "reader subs > followed blogs changed"); + getPageAdapter().refreshBlogFragments(ReaderBlogType.FOLLOWED); + } } private void performUpdate() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java index 8e9ec0895f73..acabc9bc2740 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderTagActions.java @@ -51,7 +51,7 @@ public static boolean deleteTag(final ReaderTag tag, private static boolean deleteTagsLocallyOnly(ActionListener actionListener, ReaderTag tag) { ReaderTagTable.deleteTag(tag); ReaderActions.callActionListener(actionListener, true); - EventBus.getDefault().post(new FollowedTagsFetched(true)); + EventBus.getDefault().post(new FollowedTagsFetched(true, ReaderTagTable.getFollowedTags().size())); return true; } @@ -130,7 +130,7 @@ public static boolean addTags(@NonNull final List tags, private static boolean saveTagsLocallyOnly(ActionListener actionListener, ReaderTagList newTags) { ReaderTagTable.addOrUpdateTags(newTags); ReaderActions.callActionListener(actionListener, true); - EventBus.getDefault().post(new FollowedTagsFetched(true)); + EventBus.getDefault().post(new FollowedTagsFetched(true, ReaderTagTable.getFollowedTags().size())); return true; } @@ -147,7 +147,7 @@ private static boolean saveTagsLocallyAndRemotely(ActionListener actionListener, if (actionListener != null) { ReaderActions.callActionListener(actionListener, true); } - EventBus.getDefault().post(new FollowedTagsFetched(true)); + EventBus.getDefault().post(new FollowedTagsFetched(true, ReaderTagTable.getFollowedTags().size())); }; RestRequest.ErrorListener errorListener = volleyError -> { @@ -159,7 +159,7 @@ private static boolean saveTagsLocallyAndRemotely(ActionListener actionListener, if (actionListener != null) { ReaderActions.callActionListener(actionListener, true); } - EventBus.getDefault().post(new FollowedTagsFetched(true)); + EventBus.getDefault().post(new FollowedTagsFetched(true, ReaderTagTable.getFollowedTags().size())); return; } @@ -171,7 +171,7 @@ private static boolean saveTagsLocallyAndRemotely(ActionListener actionListener, if (actionListener != null) { ReaderActions.callActionListener(actionListener, false); } - EventBus.getDefault().post(new FollowedTagsFetched(false)); + EventBus.getDefault().post(new FollowedTagsFetched(false, ReaderTagTable.getFollowedTags().size())); }; ReaderTagTable.addOrUpdateTags(newTags); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverLogic.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverLogic.kt index 9eee7c0d18bf..8e384e3626a5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverLogic.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverLogic.kt @@ -46,6 +46,7 @@ import org.wordpress.android.ui.reader.services.discover.ReaderDiscoverLogic.Dis import org.wordpress.android.ui.reader.services.discover.ReaderDiscoverLogic.DiscoverTasks.REQUEST_MORE import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T.READER +import org.wordpress.android.util.config.ReaderDiscoverNewEndpointFeatureConfig import javax.inject.Inject /** @@ -57,6 +58,7 @@ class ReaderDiscoverLogic @Inject constructor( private val getFollowedTagsUseCase: GetFollowedTagsUseCase, private val getDiscoverCardsUseCase: GetDiscoverCardsUseCase, private val appPrefsWrapper: AppPrefsWrapper, + private val readerDiscoverNewEndpointFeatureConfig: ReaderDiscoverNewEndpointFeatureConfig, ) { enum class DiscoverTasks { REQUEST_MORE, REQUEST_FIRST_PAGE @@ -116,7 +118,12 @@ class ReaderDiscoverLogic @Inject constructor( AppLog.e(READER, volleyError) resultListener.onUpdateResult(FAILED) } - WordPress.getRestClientUtilsV2()["read/tags/cards", params, null, listener, errorListener] + val endpoint = if (readerDiscoverNewEndpointFeatureConfig.isEnabled()) { + "read/streams/discover" + } else { + "read/tags/cards" + } + WordPress.getRestClientUtilsV2()[endpoint, params, null, listener, errorListener] } } @@ -200,10 +207,19 @@ class ReaderDiscoverLogic @Inject constructor( @Suppress("NestedBlockDepth") private fun createSimplifiedJson(cardsJsonArray: JSONArray, discoverTasks: DiscoverTasks): JSONArray { val simplifiedJsonList = mutableListOf() - var firstYouMayLikeCard: JSONObject? = null + var firstRecommendationCard: JSONObject? = null + val isFirstPage = discoverTasks == REQUEST_FIRST_PAGE for (i in 0 until cardsJsonArray.length()) { val cardJson = cardsJsonArray.getJSONObject(i) - when (cardJson.getString(JSON_CARD_TYPE)) { + // We should not have a recommended blogs or interests/tags card as the first element on Discover feed. + val cardType = cardJson.optString(JSON_CARD_TYPE, "") + val isCardTypeRecommendation = + cardType == JSON_CARD_RECOMMENDED_BLOGS || cardType == JSON_CARD_INTERESTS_YOU_MAY_LIKE + if (i == 0 && isFirstPage && isCardTypeRecommendation) { + firstRecommendationCard = cardJson + continue + } + when (cardType) { JSON_CARD_RECOMMENDED_BLOGS -> { cardJson.optJSONArray(JSON_CARD_DATA)?.let { recommendedBlogsCardJson -> if (recommendedBlogsCardJson.length() > 0) { @@ -212,11 +228,6 @@ class ReaderDiscoverLogic @Inject constructor( } } JSON_CARD_INTERESTS_YOU_MAY_LIKE -> { - // We should not have an interests/tags card as the first element on Discover feed. - if (i == 0 && discoverTasks == REQUEST_FIRST_PAGE) { - firstYouMayLikeCard = cardJson - continue - } simplifiedJsonList.add(cardJson) } JSON_CARD_POST -> { @@ -224,9 +235,10 @@ class ReaderDiscoverLogic @Inject constructor( } } } - // If we've received an interests/tags card as the first element, it should be displayed as the third card. - if (firstYouMayLikeCard != null) { - simplifiedJsonList.add(2, firstYouMayLikeCard) + // If we've received a recommended tags or blogs card as the first element, + // it should be displayed as the third card. + if (firstRecommendationCard != null) { + simplifiedJsonList.add(2, firstRecommendationCard) } return JSONArray(simplifiedJsonList) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java index 9f1278a5667c..151bdebd5101 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java @@ -23,7 +23,7 @@ import org.wordpress.android.models.ReaderTagType; import org.wordpress.android.ui.prefs.AppPrefs; import org.wordpress.android.ui.reader.ReaderConstants; -import org.wordpress.android.ui.reader.ReaderEvents; +import org.wordpress.android.ui.reader.ReaderEvents.FollowedBlogsFetched; import org.wordpress.android.ui.reader.ReaderEvents.FollowedTagsFetched; import org.wordpress.android.ui.reader.ReaderEvents.InterestTagsFetchEnded; import org.wordpress.android.ui.reader.services.ServiceCompletionListener; @@ -81,7 +81,7 @@ public void performTasks(EnumSet tasks, Object companion) { fetchInterestTags(); } if (tasks.contains(UpdateTask.FOLLOWED_BLOGS)) { - updateFollowedBlogs(); + updateFollowedBlogs(1, new ReaderBlogList()); } } @@ -193,7 +193,9 @@ public void run() { // broadcast the fact that there are changes didChangeFollowedTags = true; } - EventBus.getDefault().post(new FollowedTagsFetched(true, didChangeFollowedTags)); + EventBus.getDefault().post(new FollowedTagsFetched(true, + ReaderTagTable.getFollowedTags().size(), + didChangeFollowedTags)); AppPrefs.setReaderTagsUpdatedTimestamp(new Date().getTime()); taskCompleted(UpdateTask.TAGS); @@ -295,54 +297,69 @@ public void run() { /*** * request the list of blogs the current user is following */ - private void updateFollowedBlogs() { + private void updateFollowedBlogs(final int page, final ReaderBlogList serverBlogs) { RestRequest.Listener listener = new RestRequest.Listener() { @Override public void onResponse(JSONObject jsonObject) { - handleFollowedBlogsResponse(jsonObject); + handleFollowedBlogsResponse(serverBlogs, jsonObject); } }; RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() { @Override public void onErrorResponse(VolleyError volleyError) { AppLog.e(AppLog.T.READER, volleyError); + serverBlogs.clear(); taskCompleted(UpdateTask.FOLLOWED_BLOGS); } }; - AppLog.d(AppLog.T.READER, "reader service > updating followed blogs"); + AppLog.d(AppLog.T.READER, "reader service > updating followed blogs. Page requested: " + page); // request using ?meta=site,feed to get extra info - WordPress.getRestClientUtilsV1_2().get("read/following/mine?meta=site%2Cfeed", listener, errorListener); + WordPress.getRestClientUtilsV1_2() + .get("read/following/mine?number=100&page=" + page + "&meta=site%2Cfeed", listener, errorListener); } - private void handleFollowedBlogsResponse(final JSONObject jsonObject) { + private void handleFollowedBlogsResponse(final ReaderBlogList serverBlogs, final JSONObject jsonObject) { new Thread() { @Override public void run() { - ReaderBlogList serverBlogs = ReaderBlogList.fromJson(jsonObject); - ReaderBlogList localBlogs = ReaderBlogTable.getFollowedBlogs(); + ReaderBlogList currentPageServerResponse = ReaderBlogList.fromJson(jsonObject); // This is required because under rare circumstances the server can return duplicates. // We could have modified the function isSameList to eliminate the length check, // but it's better to keep it separate since we aim to remove this check as soon as possible. - removeDuplicateBlogs(serverBlogs); - - if (!localBlogs.isSameList(serverBlogs)) { - // always update the list of followed blogs if there are *any* changes between - // server and local (including subscription count, description, etc.) - ReaderBlogTable.setFollowedBlogs(serverBlogs); - // ...but only update the follow status and alert that followed blogs have - // changed if the server list doesn't have the same blogs as the local list - // (ie: a blog has been followed/unfollowed since local was last updated) - if (!localBlogs.hasSameBlogs(serverBlogs)) { - final int totalSites = jsonObject == null ? 0 : jsonObject.optInt("total_subscriptions", 0); - ReaderPostTable.updateFollowedStatus(); - AppLog.i(AppLog.T.READER, "reader blogs service > followed blogs changed"); - EventBus.getDefault().post(new ReaderEvents.FollowedBlogsChanged(totalSites)); + removeDuplicateBlogs(currentPageServerResponse); + + boolean sitesSubscribedChanged = false; + final int totalSites = jsonObject == null ? 0 : jsonObject.optInt("total_subscriptions", 0); + final int page = jsonObject == null ? 1 : jsonObject.optInt("page", 1); + final int numberOfSitesReturned = jsonObject == null ? 0 : jsonObject.optInt("number", 0); + serverBlogs.addAll(currentPageServerResponse); + if (numberOfSitesReturned > 90) { + // 90 appears to be a magic number here, and in a way, it is. + // The server doesn't always return the exact number of requested sites, likely due to deleted or + // suspended sites. In the worst-case scenario, we might make an additional request that returns 0. + updateFollowedBlogs(page + 1, serverBlogs); + } else { + ReaderBlogList localBlogs = ReaderBlogTable.getFollowedBlogs(); + if (!localBlogs.isSameList(serverBlogs)) { + // always update the list of followed blogs if there are *any* changes between + // server and local (including subscription count, description, etc.) + ReaderBlogTable.setFollowedBlogs(serverBlogs); + // ...but only update the follow status and alert that followed blogs have + // changed if the server list doesn't have the same blogs as the local list + // (ie: a blog has been followed/unfollowed since local was last updated) + if (!localBlogs.hasSameBlogs(serverBlogs)) { + ReaderPostTable.updateFollowedStatus(); + AppLog.i(AppLog.T.READER, "reader blogs service > followed blogs changed: " + + totalSites); + sitesSubscribedChanged = true; + } } + EventBus.getDefault().post(new FollowedBlogsFetched(totalSites, sitesSubscribedChanged)); + serverBlogs.clear(); + taskCompleted(UpdateTask.FOLLOWED_BLOGS); } - - taskCompleted(UpdateTask.FOLLOWED_BLOGS); } }.start(); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index a9dd37639a36..5f806bb6ad8b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -400,9 +400,11 @@ class SubFilterViewModel @Inject constructor( @Suppress("unused", "UNUSED_PARAMETER") @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: ReaderEvents.FollowedBlogsChanged) { - AppLog.d(T.READER, "Subfilter bottom sheet > followed blogs changed") - loadSubFilters() + fun onEventMainThread(event: ReaderEvents.FollowedBlogsFetched) { + if(event.didChange()) { + AppLog.d(T.READER, "Subfilter bottom sheet > followed blogs changed") + loadSubFilters() + } } override fun onCleared() { 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 d935b29b57a9..2266d20b391f 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 @@ -399,6 +399,21 @@ class ReaderTracker @Inject constructor( } } + private fun trackFollowedCount(type: String, numberOfItems: Int) { + val props: MutableMap = HashMap() + props["type"] = type + props["count"] = numberOfItems.toString() + AnalyticsTracker.track(Stat.READER_FOLLOWING_FETCHED, props) + } + + fun trackFollowedTagsCount(numberOfItems: Int) { + trackFollowedCount("tags", numberOfItems) + } + + fun trackSubscribedSitesCount(numberOfItems: Int) { + trackFollowedCount("sites", numberOfItems) + } + /* HELPER */ @JvmOverloads @@ -466,23 +481,6 @@ class ReaderTracker @Inject constructor( AnalyticsTracker.track(stat, properties) } - private fun trackFollowedCount(type: String, numberOfItems: Int) { - val props: MutableMap = HashMap() - props["type"] = type - props["count"] = numberOfItems.toString() - AnalyticsTracker.track(Stat.READER_FOLLOWING_FETCHED, props) - } - - @JvmStatic - fun trackFollowedTagsCount(numberOfItems: Int) { - trackFollowedCount("tags", numberOfItems) - } - - @JvmStatic - fun trackSubscribedSitesCount(numberOfItems: Int) { - trackFollowedCount("sites", numberOfItems) - } - fun isUserProfileSource(source: String): Boolean { return (source == SOURCE_READER_LIKE_LIST_USER_PROFILE || source == SOURCE_NOTIF_LIKE_LIST_USER_PROFILE || diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt index 69a02b9d7508..afa95b1e79a8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt @@ -699,7 +699,7 @@ class ReaderPostDetailViewModel @Inject constructor( } private fun buildLikersUiState(updateLikesState: GetLikesState?): TrainOfFacesUiState { - val (likers, numLikes, iLiked) = getLikersEssentials(updateLikesState) + val (likers, numLikes) = getLikersEssentials(updateLikesState) val showLoading = updateLikesState is Loading var showEmptyState = false @@ -717,7 +717,7 @@ class ReaderPostDetailViewModel @Inject constructor( } ?: false val engageItemsList = if (showLikeFacesTrainContainer) { - likers + getLikersFacesText(showEmptyState, numLikes, iLiked) + likers + getLikersFacesText(showEmptyState, numLikes) } else { listOf() } @@ -750,56 +750,25 @@ class ReaderPostDetailViewModel @Inject constructor( } @Suppress("LongMethod") - private fun getLikersFacesText(showEmptyState: Boolean, numLikes: Int, iLiked: Boolean): List { + private fun getLikersFacesText(showEmptyState: Boolean, numLikes: Int): List { @AttrRes val labelColor = R.attr.wpColorOnSurfaceMedium return when { showEmptyState -> { listOf() } - numLikes == 1 && iLiked -> { + numLikes == 1 -> { TrailingLabelTextItem( UiStringText( - htmlMessageUtils.getHtmlMessageFromStringFormatResId(R.string.like_faces_you_like_text) + htmlMessageUtils.getHtmlMessageFromStringFormatResId(R.string.like_title_singular) ), labelColor ).toList() } - numLikes == 2 && iLiked -> { + numLikes > 1 -> { TrailingLabelTextItem( UiStringText( htmlMessageUtils.getHtmlMessageFromStringFormatResId( - R.string.like_faces_you_plus_one_like_text - ) - ), - labelColor - ).toList() - } - numLikes > 2 && iLiked -> { - TrailingLabelTextItem( - UiStringText( - htmlMessageUtils.getHtmlMessageFromStringFormatResId( - R.string.like_faces_you_plus_others_like_text, - numLikes - 1 - ) - ), - labelColor - ).toList() - } - numLikes == 1 && !iLiked -> { - TrailingLabelTextItem( - UiStringText( - htmlMessageUtils.getHtmlMessageFromStringFormatResId( - R.string.like_faces_one_blogger_likes_text - ) - ), - labelColor - ).toList() - } - numLikes > 1 && !iLiked -> { - TrailingLabelTextItem( - UiStringText( - htmlMessageUtils.getHtmlMessageFromStringFormatResId( - R.string.like_faces_others_like_text, + R.string.like_title_plural, numLikes ) ), diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt index 4232579bd121..5806d169043a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderViewModel.kt @@ -31,6 +31,7 @@ import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil import org.wordpress.android.ui.jetpackoverlay.JetpackOverlayConnectedFeature.READER import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository +import org.wordpress.android.ui.prefs.AppPrefs import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.quickstart.QuickStartEvent import org.wordpress.android.ui.reader.ReaderEvents @@ -60,6 +61,7 @@ import kotlin.coroutines.CoroutineContext const val UPDATE_TAGS_THRESHOLD = 1000 * 60 * 60 // 1 hr const val TRACK_TAB_CHANGED_THROTTLE = 100L +const val ONE_HOUR_MILLIS = 1000 * 60 * 60 @Suppress("ForbiddenComment") class ReaderViewModel @Inject constructor( @@ -220,6 +222,29 @@ class ReaderViewModel @Inject constructor( @Subscribe(threadMode = MAIN) fun onTagsUpdated(event: ReaderEvents.FollowedTagsFetched) { loadTabs() + // Determine if analytics should be bumped either due to tags changed or time elapsed since last bump + val now = DateProvider().getCurrentDate().time + val shouldBumpAnalytics = event.didChange() + || ( now - appPrefsWrapper.readerAnalyticsCountTagsTimestamp > ONE_HOUR_MILLIS) + + if (shouldBumpAnalytics) { + readerTracker.trackFollowedTagsCount(event.totalTags) + appPrefsWrapper.readerAnalyticsCountTagsTimestamp = now + } + } + + @Suppress("unused", "UNUSED_PARAMETER") + @Subscribe(threadMode = MAIN) + fun onSubscribedSitesUpdated(event: ReaderEvents.FollowedBlogsFetched) { + // Determine if analytics should be bumped either due to sites changed or time elapsed since last bump + val now = DateProvider().getCurrentDate().time + val shouldBumpAnalytics = event.didChange() + || (now - AppPrefs.getReaderAnalyticsCountSitesTimestamp() > ONE_HOUR_MILLIS) + + if (shouldBumpAnalytics) { + readerTracker.trackSubscribedSitesCount(event.totalSubscriptions) + AppPrefs.setReaderAnalyticsCountSitesTimestamp(now) + } } fun onScreenInForeground() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt index 579f97bc187f..c54010baac66 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/review/ReviewViewModel.kt @@ -5,19 +5,15 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.wordpress.android.ui.prefs.AppPrefsWrapper -import org.wordpress.android.util.config.InAppReviewsFeatureConfig import org.wordpress.android.viewmodel.Event import javax.inject.Inject -class ReviewViewModel @Inject constructor( - private val appPrefsWrapper: AppPrefsWrapper, - private val inAppReviewsFeatureConfig: InAppReviewsFeatureConfig -) : ViewModel() { +class ReviewViewModel @Inject constructor(private val appPrefsWrapper: AppPrefsWrapper) : ViewModel() { private val _launchReview = MutableLiveData>() val launchReview = _launchReview as LiveData> fun onPublishingPost(isFirstTimePublishing: Boolean) { - if (inAppReviewsFeatureConfig.isEnabled() && !appPrefsWrapper.isInAppReviewsShown() && isFirstTimePublishing) { + if (!appPrefsWrapper.isInAppReviewsShown() && isFirstTimePublishing) { if (appPrefsWrapper.getPublishedPostCount() < TARGET_COUNT_POST_PUBLISHED) { appPrefsWrapper.incrementPublishedPostCount() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt index 764f854b6190..e0bcad563e19 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt @@ -177,6 +177,7 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( private fun buildDataSet(context: Context, cut: List): BarDataSet { val dataSet = BarDataSet(cut, "Data") + chart.renderer.paintRender.shader = null dataSet.color = ContextCompat.getColor(context, R.color.blue_50) dataSet.formLineWidth = 0f dataSet.setDrawValues(false) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/viewholders/BarChartViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/viewholders/BarChartViewHolder.kt index 33e93e01465f..e459f53c6883 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/viewholders/BarChartViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/viewholders/BarChartViewHolder.kt @@ -229,16 +229,8 @@ class BarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( private fun buildDataSet(context: Context, cut: List): BarDataSet { val dataSet = BarDataSet(cut, "Data") + chart.renderer.paintRender.shader = null dataSet.color = ContextCompat.getColor(context, R.color.stats_bar_chart_top) - dataSet.setGradientColor( - ContextCompat.getColor( - context, - R.color.stats_bar_chart_top - ), ContextCompat.getColor( - context, - R.color.stats_bar_chart_top - ) - ) dataSet.formLineWidth = 0f dataSet.setDrawValues(false) dataSet.isHighlightEnabled = true @@ -252,16 +244,8 @@ class BarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( private fun buildOverlappingDataSet(context: Context, cut: List): BarDataSet { val dataSet = BarDataSet(cut, "Overlapping data") + chart.renderer.paintRender.shader = null dataSet.color = ContextCompat.getColor(context, R.color.primary_60) - dataSet.setGradientColor( - ContextCompat.getColor( - context, - R.color.stats_bar_chart_bottom - ), ContextCompat.getColor( - context, - R.color.stats_bar_chart_bottom - ) - ) dataSet.formLineWidth = 0f dataSet.setDrawValues(false) dataSet.isHighlightEnabled = true @@ -279,16 +263,8 @@ class BarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( BarEntry(it.x, maxEntry.y, it.data) } val dataSet = BarDataSet(highlightedDataSet, "Highlight") + chart.renderer.paintRender.shader = null dataSet.color = ContextCompat.getColor(context, AndroidR.color.transparent) - dataSet.setGradientColor( - ContextCompat.getColor( - context, - AndroidR.color.transparent - ), ContextCompat.getColor( - context, - AndroidR.color.transparent - ) - ) dataSet.formLineWidth = 0f dataSet.isHighlightEnabled = true dataSet.highLightColor = ContextCompat.getColor( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt index 1e092d4f2812..2193ff2d00b6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt @@ -6,7 +6,6 @@ import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.ui.stats.refresh.StatsViewModel.DateSelectorUiModel import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider.SelectedDate -import org.wordpress.android.util.config.StatsTrafficTabFeatureConfig import org.wordpress.android.util.perform import javax.inject.Inject @@ -16,8 +15,7 @@ constructor( private val statsDateFormatter: StatsDateFormatter, private val siteProvider: StatsSiteProvider, var statsGranularity: StatsGranularity, - private val isGranularitySpinnerVisible: Boolean, - private val statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig + private val isGranularitySpinnerVisible: Boolean ) { private val _dateSelectorUiModel = MutableLiveData() val dateSelectorData: LiveData = _dateSelectorUiModel @@ -35,18 +33,13 @@ constructor( fun updateDateSelector() { val updatedDate = getDateLabelForSection() val currentState = dateSelectorData.value - val timeZone = if (statsTrafficTabFeatureConfig.isEnabled()) { - null - } else { - statsDateFormatter.printTimeZone(siteProvider.siteModel) - } val updatedState = DateSelectorUiModel( true, isGranularitySpinnerVisible, updatedDate, enableSelectPrevious = selectedDateProvider.hasPreviousDate(statsGranularity), enableSelectNext = selectedDateProvider.hasNextDate(statsGranularity), - timeZone = timeZone + timeZone = statsDateFormatter.printTimeZone(siteProvider.siteModel) ) emitValue(currentState, updatedState) } @@ -87,8 +80,7 @@ constructor( @Inject constructor( private val selectedDateProvider: SelectedDateProvider, private val siteProvider: StatsSiteProvider, - private val statsDateFormatter: StatsDateFormatter, - private val statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig + private val statsDateFormatter: StatsDateFormatter ) { fun build(statsGranularity: StatsGranularity, isGranularitySpinnerVisible: Boolean = false): StatsDateSelector { return StatsDateSelector( @@ -96,8 +88,7 @@ constructor( statsDateFormatter, siteProvider, statsGranularity, - isGranularitySpinnerVisible, - statsTrafficTabFeatureConfig + isGranularitySpinnerVisible ) } } 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 deleted file mode 100644 index 9bb5777f063d..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/util/config/InAppReviewsFeatureConfig.kt +++ /dev/null @@ -1,16 +0,0 @@ -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/ReaderDiscoverNewEndpointFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderDiscoverNewEndpointFeatureConfig.kt new file mode 100644 index 000000000000..b2d4ebc72a74 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/ReaderDiscoverNewEndpointFeatureConfig.kt @@ -0,0 +1,23 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.Feature +import javax.inject.Inject + +private const val READER_DISCOVER_NEW_ENDPOINT_REMOTE_FIELD = "reader_discover_new_endpoint" + +@Feature( + remoteField = READER_DISCOVER_NEW_ENDPOINT_REMOTE_FIELD, + defaultValue = true, +) +class ReaderDiscoverNewEndpointFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.READER_DISCOVER_NEW_ENDPOINT, + READER_DISCOVER_NEW_ENDPOINT_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/SyncPublishingFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/SyncPublishingFeatureConfig.kt new file mode 100644 index 000000000000..e1a4580e3b20 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/SyncPublishingFeatureConfig.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 SYNC_PUBLISHING_FEATURE_REMOTE_FIELD = "sync_publishing" + +@Feature(SYNC_PUBLISHING_FEATURE_REMOTE_FIELD, false) +class SyncPublishingFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.SYNC_PUBLISHING, + SYNC_PUBLISHING_FEATURE_REMOTE_FIELD +) diff --git a/WordPress/src/main/res/layout/new_edit_post_activity.xml b/WordPress/src/main/res/layout/new_edit_post_activity.xml index b619e8b65803..d65437835efa 100644 --- a/WordPress/src/main/res/layout/new_edit_post_activity.xml +++ b/WordPress/src/main/res/layout/new_edit_post_activity.xml @@ -65,6 +65,44 @@ tools:context=".ui.photopicker.PhotoPickerFragment" tools:visibility="visible" /> + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/trailing_label_item.xml b/WordPress/src/main/res/layout/trailing_label_item.xml index 6b67448945aa..07823e955b79 100644 --- a/WordPress/src/main/res/layout/trailing_label_item.xml +++ b/WordPress/src/main/res/layout/trailing_label_item.xml @@ -16,6 +16,6 @@ android:paddingBottom="@dimen/margin_medium" android:paddingTop="@dimen/margin_medium" android:textAppearance="?attr/textAppearanceCaption" - tools:text="19 bloggers like this." /> + tools:text="19 likes" /> diff --git a/WordPress/src/main/res/values/colors.xml b/WordPress/src/main/res/values/colors.xml index c75fad414336..8e803301f886 100644 --- a/WordPress/src/main/res/values/colors.xml +++ b/WordPress/src/main/res/values/colors.xml @@ -158,4 +158,8 @@ #F2F2F7 #2C2C2E + + + #EDD6C5 + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 82adbf31dd1d..ef4354f0bdd0 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1629,14 +1629,6 @@ 1 Like %d Likes Error loading like data. %s. - - <a href="">You</a> like this. - <a href="">You and 1 blogger</a> like this. - <a href="">You and %1$s bloggers</a> like this. - <a href="">1 blogger</a> likes this. - <a href="">%1$s bloggers</a> like this. Reader @@ -1720,6 +1712,8 @@ Your draft is uploading Post converted back to draft Failed to insert media.\nPlease tap to retry. + Updating post content + Failed to update post content Post settings diff --git a/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/CampaignDetailViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/CampaignDetailViewModelTest.kt index c36097d3e3c7..bdf50491b4b4 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/CampaignDetailViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/CampaignDetailViewModelTest.kt @@ -77,7 +77,7 @@ class CampaignDetailViewModelTest : BaseUnitTest() { } @Test fun `given valid campaignId and pageSource, when start is called, then trackCampaignDetailsOpened is called`() { - viewModel.start(1, CampaignDetailPageSource.DASHBOARD_CARD) + viewModel.start(campaignId = "1", CampaignDetailPageSource.DASHBOARD_CARD) verify(blazeFeatureUtils).trackCampaignDetailsOpened(any()) } @@ -89,7 +89,7 @@ class CampaignDetailViewModelTest : BaseUnitTest() { val uiState = mutableListOf() val actionEvents = mutableListOf() testWithData(actionEvents, uiState) { - viewModel.start(1, CampaignDetailPageSource.DASHBOARD_CARD) + viewModel.start(campaignId = "1", CampaignDetailPageSource.DASHBOARD_CARD) assertThat(uiState.last()).isInstanceOf(CampaignDetailUiState.GenericError::class.java) } @@ -102,7 +102,7 @@ class CampaignDetailViewModelTest : BaseUnitTest() { val uiState = mutableListOf() val actionEvents = mutableListOf() testWithData(actionEvents, uiState) { - viewModel.start(1, CampaignDetailPageSource.DASHBOARD_CARD) + viewModel.start(campaignId = "1", CampaignDetailPageSource.DASHBOARD_CARD) assertThat(uiState.last()).isInstanceOf(CampaignDetailUiState.GenericError::class.java) } @@ -122,7 +122,7 @@ class CampaignDetailViewModelTest : BaseUnitTest() { val uiStates = mutableListOf() val actionEvents = mutableListOf() testWithData(actionEvents, uiStates) { - viewModel.start(1, CampaignDetailPageSource.DASHBOARD_CARD) + viewModel.start(campaignId = "1", CampaignDetailPageSource.DASHBOARD_CARD) assertThat(uiStates.last()).isInstanceOf(CampaignDetailUiState.Prepared::class.java) } @@ -135,7 +135,7 @@ class CampaignDetailViewModelTest : BaseUnitTest() { val uiStates = mutableListOf() val actionEvents = mutableListOf() testWithData(actionEvents, uiStates) { - viewModel.start(1, CampaignDetailPageSource.DASHBOARD_CARD) + viewModel.start(campaignId = "1", CampaignDetailPageSource.DASHBOARD_CARD) assertThat(uiStates.first()).isInstanceOf(CampaignDetailUiState.Preparing::class.java) } @@ -177,7 +177,7 @@ class CampaignDetailViewModelTest : BaseUnitTest() { whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false) testWithData(actionEvents, uiStates) { - viewModel.start(1, CampaignDetailPageSource.DASHBOARD_CARD) + viewModel.start(campaignId = "1", CampaignDetailPageSource.DASHBOARD_CARD) val uiState = uiStates.last() assertThat(uiState).isInstanceOf(CampaignDetailUiState.NoNetworkError::class.java) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/CampaignListingViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/CampaignListingViewModelTest.kt index e432ebf539d1..7b191cad43c4 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/CampaignListingViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/CampaignListingViewModelTest.kt @@ -114,11 +114,11 @@ class CampaignListingViewModelTest : BaseUnitTest() { @Test fun `given no campaigns in db + api, when viewmodel start, then should show no campaigns error`() = runTest { - val noCampaigns: Result> = Result.Failure(NoCampaigns) + val noCampaigns = Result.Failure(NoCampaigns) whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) whenever(selectedSiteRepository.getSelectedSite()).thenReturn(siteModel) whenever(getCampaignListFromDbUseCase.execute(siteModel)).thenReturn(noCampaigns) - whenever(fetchCampaignListUseCase.execute(siteModel, 1)).thenReturn(noCampaigns) + whenever(fetchCampaignListUseCase.execute(siteModel, 0)).thenReturn(noCampaigns) viewModel.start(CampaignListingPageSource.DASHBOARD_CARD) advanceUntilIdle() @@ -129,10 +129,10 @@ class CampaignListingViewModelTest : BaseUnitTest() { @Test fun `given no campaigns in db + api, when click is invoked on create, then navigate to blaze flow`() = runTest { - val noCampaigns: Result> = Result.Failure(NoCampaigns) + val noCampaigns = Result.Failure(NoCampaigns) whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) whenever(getCampaignListFromDbUseCase.execute(siteModel)).thenReturn(noCampaigns) - whenever(fetchCampaignListUseCase.execute(siteModel, 1)).thenReturn(noCampaigns) + whenever(fetchCampaignListUseCase.execute(siteModel, offset = 0)).thenReturn(noCampaigns) viewModel.start(CampaignListingPageSource.DASHBOARD_CARD) advanceUntilIdle() diff --git a/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapperTest.kt index 67a3718fd0b0..50abe4e137ee 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/CampaignListingUIModelMapperTest.kt @@ -30,16 +30,17 @@ class CampaignListingUIModelMapperTest : BaseUnitTest() { } private val activeCampaign = BlazeCampaignModel( - campaignId = 1, + campaignId = "1", title = "title", - uiStatus = "active", imageUrl = "imageUrl", - impressions = 1L, - clicks = 1L, - budgetCents = 100, - createdAt = mock(), - endDate = mock(), + startTime = mock(), + durationInDays = 1, + uiStatus = "active", + impressions = 1, + clicks = 1, targetUrn = null, + totalBudget = 1.0, + spentBudget = 0.0, ) @Test @@ -59,16 +60,17 @@ class CampaignListingUIModelMapperTest : BaseUnitTest() { } private val inActiveCampaign = BlazeCampaignModel( - campaignId = 1, + campaignId = "1", title = "title", - uiStatus = "canceled", imageUrl = "imageUrl", + startTime = mock(), + durationInDays = 1, + uiStatus = "canceled", impressions = 0, clicks = 0, - budgetCents = 100, - createdAt = mock(), - endDate = mock(), targetUrn = null, + totalBudget = 0.0, + spentBudget = 0.0, ) @Test diff --git a/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/FetchCampaignListUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/FetchCampaignListUseCaseTest.kt index 07cc6dee86a6..11c8164ba162 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/FetchCampaignListUseCaseTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/FetchCampaignListUseCaseTest.kt @@ -21,7 +21,7 @@ import org.wordpress.android.fluxc.store.blaze.BlazeCampaignsStore @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) -class FetchCampaignListUseCaseTest: BaseUnitTest() { +class FetchCampaignListUseCaseTest : BaseUnitTest() { @Mock lateinit var store: BlazeCampaignsStore @@ -38,26 +38,28 @@ class FetchCampaignListUseCaseTest: BaseUnitTest() { @Test fun `given store returns error, when usecase execute, returns generic error`() = runTest { val siteModel = mock() - val page = 1 - whenever(store.fetchBlazeCampaigns(siteModel, page)).thenReturn( + val offset = 0 + whenever(store.fetchBlazeCampaigns(siteModel, offset, PER_PAGE)).thenReturn( BlazeCampaignsStore.BlazeCampaignsResult(BlazeCampaignsError(BlazeCampaignsErrorType.INVALID_RESPONSE)) ) - val actualResult = fetchCampaignListUseCase.execute(siteModel, page) + val actualResult = fetchCampaignListUseCase.execute(siteModel, offset) assertThat(actualResult is Result.Failure).isTrue - assertThat((actualResult as Result.Failure).value).isEqualTo(GenericError) + assertThat((actualResult as Result.Failure).value).isEqualTo(GenericResult) } @Test fun `given store returns empty campaigns, when usecase execute, returns no campaigns error`() = runTest { val siteModel = mock() - val page = 1 - whenever(store.fetchBlazeCampaigns(siteModel, page)).thenReturn(BlazeCampaignsStore.BlazeCampaignsResult( - BlazeCampaignsModel(emptyList(),1,0,1) - )) + val offset = 0 + whenever(store.fetchBlazeCampaigns(siteModel, offset, PER_PAGE)).thenReturn( + BlazeCampaignsStore.BlazeCampaignsResult( + BlazeCampaignsModel(campaigns = emptyList(), skipped = 0, totalItems = 1) + ) + ) - val actualResult = fetchCampaignListUseCase.execute(siteModel, page) + val actualResult = fetchCampaignListUseCase.execute(siteModel, offset) assertThat(actualResult is Result.Failure).isTrue assertThat((actualResult as Result.Failure).value).isEqualTo(NoCampaigns) @@ -66,14 +68,20 @@ class FetchCampaignListUseCaseTest: BaseUnitTest() { @Test fun `given store returns campaigns, when usecase execute, returns campaigns`() = runTest { val siteModel = mock() - val page = 1 - whenever(store.fetchBlazeCampaigns(siteModel, page)).thenReturn(BlazeCampaignsStore.BlazeCampaignsResult( - BlazeCampaignsModel(mock(),1,0,1) - )) + val offset = 0 + whenever(store.fetchBlazeCampaigns(siteModel, offset, PER_PAGE)).thenReturn( + BlazeCampaignsStore.BlazeCampaignsResult( + BlazeCampaignsModel(campaigns = mock(), skipped = 0, totalItems = 1) + ) + ) whenever(mapper.mapToCampaignModels(any())).thenReturn(mock()) - val actualResult = fetchCampaignListUseCase.execute(siteModel, page) + val actualResult = fetchCampaignListUseCase.execute(siteModel, offset) assertThat(actualResult is Result.Success).isTrue } + + companion object { + const val PER_PAGE = 10 + } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/GetCampaignListFromDbUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/GetCampaignListFromDbUseCaseTest.kt index fba85dfa6ea2..35b77997ae84 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/GetCampaignListFromDbUseCaseTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/blaze/blazecampaigns/campaignlisting/GetCampaignListFromDbUseCaseTest.kt @@ -14,7 +14,6 @@ import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.Result import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.model.blaze.BlazeCampaignsModel import org.wordpress.android.fluxc.store.blaze.BlazeCampaignsStore @ExperimentalCoroutinesApi @@ -36,9 +35,7 @@ class GetCampaignListFromDbUseCaseTest: BaseUnitTest() { @Test fun `given store returns empty campaigns, when usecase execute, returns no campaigns error`() = runTest { val siteModel = mock() - whenever(store.getBlazeCampaigns(siteModel)).thenReturn( - BlazeCampaignsModel(emptyList(), 1, 0, 1) - ) + whenever(store.getBlazeCampaigns(siteModel)).thenReturn(emptyList()) val actualResult = getCampaignListFromDbUseCase.execute(siteModel) @@ -49,7 +46,7 @@ class GetCampaignListFromDbUseCaseTest: BaseUnitTest() { @Test fun `given store returns campaigns, when usecase execute, returns campaigns `() = runTest { val siteModel = mock() - whenever(store.getBlazeCampaigns(siteModel)).thenReturn(BlazeCampaignsModel(mock(),1,0,1)) + whenever(store.getBlazeCampaigns(siteModel)).thenReturn(mock()) whenever(mapper.mapToCampaignModels(any())).thenReturn(mock()) val actualResult = getCampaignListFromDbUseCase.execute(siteModel) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSliceTest.kt index 99cac4cd4914..f6a9f5e3b71a 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSliceTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/BlazeCardViewModelSliceTest.kt @@ -39,7 +39,7 @@ class BlazeCardViewModelSliceTest : BaseUnitTest() { private lateinit var refreshActions: MutableList - private val campaignId = 1 + private val campaignId = "1" @Before fun setup() { diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/blaze/BlazeCardBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/blaze/BlazeCardBuilderTest.kt index 5a3a24657ec1..9d13daeb6516 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/blaze/BlazeCardBuilderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/blaze/BlazeCardBuilderTest.kt @@ -24,16 +24,17 @@ import org.wordpress.android.ui.utils.ListItemInteraction import org.wordpress.android.ui.utils.UiString val campaign = BlazeCampaignModel( - campaignId = 1, + campaignId = "1234", title = "title", imageUrl = "imageUrl", - createdAt = mock(), - endDate = mock(), + startTime = mock(), + durationInDays = 1, uiStatus = "active", - budgetCents = 20L, impressions = 1, clicks = 1, targetUrn = null, + totalBudget = 0.0, + spentBudget = 0.0, ) val onCreateCampaignClick = { } @@ -44,7 +45,7 @@ val onMoreMenuClick = { } val onLearnMoreItemClick = { } val viewAllCampaignsClick = { } -private var onCampaignClick: ((campaignId: Int) -> Unit) = { } +private var onCampaignClick: ((campaignId: String) -> Unit) = { } val campaignWithBlazeBuilderParams = CampaignWithBlazeCardBuilderParams( campaign = campaign, onCardClick = onCardClick, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/blaze/BlazeCardSourceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/blaze/BlazeCardSourceTest.kt index 4fe2e5de37bb..964ec64f5a3c 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/blaze/BlazeCardSourceTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/blaze/BlazeCardSourceTest.kt @@ -16,7 +16,7 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.blaze.BlazeCampaignModel import org.wordpress.android.ui.blaze.BlazeFeatureUtils import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.FetchCampaignListUseCase -import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.GenericError +import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.GenericResult import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.BlazeCardUpdate import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.mysite.cards.blaze.BlazeCardSource @@ -129,7 +129,7 @@ class BlazeCardSourceTest : BaseUnitTest() { blazeCardSource.build(testScope(), SITE_LOCAL_ID) .observeForever { it?.let { result.add(it) } } - verify(fetchCampaignListUseCase, never()).execute(siteModel,1) + verify(fetchCampaignListUseCase, never()).execute(siteModel,offset = 0) assertThat(result.last()).isEqualTo(BlazeCardUpdate(true, null)) } @@ -145,7 +145,7 @@ class BlazeCardSourceTest : BaseUnitTest() { blazeCardSource.build(testScope(), SITE_LOCAL_ID) .observeForever { it?.let { result.add(it) } } - verify(fetchCampaignListUseCase, never()).execute(siteModel,1) + verify(fetchCampaignListUseCase, never()).execute(siteModel,offset = 0) assertThat(result.last()).isEqualTo(BlazeCardUpdate(true, campaignInDb)) } @@ -155,7 +155,7 @@ class BlazeCardSourceTest : BaseUnitTest() { val result = mutableListOf() whenever(blazeFeatureUtils.shouldShowBlazeCampaigns()).thenReturn(true) whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) - whenever(fetchCampaignListUseCase.execute(siteModel,1)).thenReturn(Result.Failure(GenericError)) + whenever(fetchCampaignListUseCase.execute(siteModel,offset = 0)).thenReturn(Result.Failure(GenericResult)) blazeCardSource.build(testScope(), SITE_LOCAL_ID) .observeForever { it?.let { result.add(it) } } @@ -171,7 +171,7 @@ class BlazeCardSourceTest : BaseUnitTest() { val campaignInDb = mock() whenever(blazeFeatureUtils.shouldShowBlazeCampaigns()).thenReturn(true) whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) - whenever(fetchCampaignListUseCase.execute(siteModel,1)).thenReturn(Result.Success(mock())) + whenever(fetchCampaignListUseCase.execute(siteModel,offset = 0)).thenReturn(Result.Success(mock())) whenever(mostRecentCampaignUseCase.execute(siteModel)).thenReturn(Result.Success(campaignInDb)) blazeCardSource.build(testScope(), SITE_LOCAL_ID) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostFreshnessCheckerImplTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostFreshnessCheckerImplTest.kt new file mode 100644 index 000000000000..639c3312cdcd --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostFreshnessCheckerImplTest.kt @@ -0,0 +1,39 @@ +package org.wordpress.android.ui.posts + +import org.junit.Test +import org.mockito.kotlin.mock +import org.wordpress.android.fluxc.model.PostImmutableModel +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class PostFreshnessCheckerImplTest { + private val fixedCurrentTimeMillis = 100_000L // Example current time in milliseconds + private val timeProvider = object : TimeProvider { + override fun currentTimeMillis() = fixedCurrentTimeMillis + } + + private val postFreshnessChecker = PostFreshnessCheckerImpl(timeProvider) + + @Test + fun `should refresh post when post is older than cache validity`() { + // Post timestamp is set to simulate being older than the cache validity + val postTimestamp = fixedCurrentTimeMillis - PostFreshnessCheckerImpl.CACHE_VALIDITY_MILLIS - 1 + val post = mock { + on { dbTimestamp }.thenReturn(postTimestamp) + } + + // Adjust the system time or post creation time as needed to reflect the scenario being tested + assertTrue(postFreshnessChecker.shouldRefreshPost(post)) + } + + @Test + fun `should not refresh post when post is within cache validity`() { + // Post timestamp is set to simulate being within the cache validity period + val postTimestamp = fixedCurrentTimeMillis - PostFreshnessCheckerImpl.CACHE_VALIDITY_MILLIS + 1 + val post = mock { + on { dbTimestamp }.thenReturn(postTimestamp) + } + + assertFalse(postFreshnessChecker.shouldRefreshPost(post)) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/StorePostViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/StorePostViewModelTest.kt index c4ca4d62a19b..2d5a52692ea0 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/StorePostViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/editor/StorePostViewModelTest.kt @@ -23,6 +23,7 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.ui.posts.EditPostRepository import org.wordpress.android.ui.posts.EditPostRepository.UpdatePostResult +import org.wordpress.android.ui.posts.IPostFreshnessChecker import org.wordpress.android.ui.posts.PostUtilsWrapper import org.wordpress.android.ui.posts.SavePostToDbUseCase import org.wordpress.android.ui.posts.editor.StorePostViewModel.ActivityFinishState.SAVED_LOCALLY @@ -59,6 +60,9 @@ class StorePostViewModelTest : BaseUnitTest() { @Mock lateinit var context: Context + @Mock + lateinit var postFreshnessChecker: IPostFreshnessChecker + private lateinit var viewModel: StorePostViewModel private val title = "title" private val updatedTitle = "updatedTitle" @@ -79,7 +83,8 @@ class StorePostViewModelTest : BaseUnitTest() { uploadService, savePostToDbUseCase, networkUtils, - dispatcher + dispatcher, + postFreshnessChecker ) postModel.setId(postId) postModel.setTitle(title) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/FetchFollowedTagsUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/FetchFollowedTagsUseCaseTest.kt index 5c0c086000e0..d117e66fd7ba 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/FetchFollowedTagsUseCaseTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/FetchFollowedTagsUseCaseTest.kt @@ -64,7 +64,7 @@ class FetchFollowedTagsUseCaseTest : BaseUnitTest() { fun `Success returned when FollowedTagsFetched event is posted with success`() = test { // Given whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) - val event = FollowedTagsFetched(true) + val event = FollowedTagsFetched(true, 10) whenever(readerUpdateServiceStarterWrapper.startService(contextProvider.getContext(), EnumSet.of(TAGS))) .then { useCase.onFollowedTagsFetched(event) } @@ -79,7 +79,7 @@ class FetchFollowedTagsUseCaseTest : BaseUnitTest() { fun `RemoteRequestFailure returned when FollowedTagsFetched event is posted with failure`() = test { // Given whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) - val event = FollowedTagsFetched(false) + val event = FollowedTagsFetched(false, 10) whenever(readerUpdateServiceStarterWrapper.startService(contextProvider.getContext(), EnumSet.of(TAGS))) .then { useCase.onFollowedTagsFetched(event) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderDiscoverDataProviderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderDiscoverDataProviderTest.kt index 3a87558edee4..3a761b2108b5 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderDiscoverDataProviderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/repository/ReaderDiscoverDataProviderTest.kt @@ -251,7 +251,8 @@ class ReaderDiscoverDataProviderTest : BaseUnitTest() { // Act dataProvider.onFollowedTagsFetched( FollowedTagsFetched( - true + true, + 10 ) ) // Assert diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt index 071880f7d400..e603e69a5e84 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt @@ -978,7 +978,7 @@ class ReaderPostDetailViewModelTest : BaseUnitTest() { fun `ui state show likers faces when data available`() { val likesState = getGetLikesState(TEST_CONFIG_1) as LikesData val likers = MutableList(5) { mock() } - val testTextString = "10 bloggers like this." + val testTextString = "10 likes" getLikesState.value = likesState whenever(accountStore.account).thenReturn(AccountModel().apply { userId = -1 }) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt index be459d325fe5..dc83739ec55d 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/review/ReviewViewModelTest.kt @@ -10,7 +10,6 @@ import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.whenever import org.wordpress.android.eventToList import org.wordpress.android.ui.prefs.AppPrefsWrapper -import org.wordpress.android.util.config.InAppReviewsFeatureConfig import kotlin.test.assertEquals @RunWith(MockitoJUnitRunner::class) @@ -19,9 +18,6 @@ class ReviewViewModelTest { @JvmField val rule = InstantTaskExecutorRule() - @Mock - lateinit var inAppReviewsFeatureConfig: InAppReviewsFeatureConfig - @Mock lateinit var appPrefsWrapper: AppPrefsWrapper @@ -31,8 +27,7 @@ class ReviewViewModelTest { @Before fun setup() { - whenever(inAppReviewsFeatureConfig.isEnabled()).thenReturn(true) - viewModel = ReviewViewModel(appPrefsWrapper, inAppReviewsFeatureConfig) + viewModel = ReviewViewModel(appPrefsWrapper) events = mutableListOf() events = viewModel.launchReview.eventToList() } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt index 48ba3faf6764..20dd3ec82873 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt @@ -15,7 +15,6 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDa import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter import org.wordpress.android.ui.stats.refresh.utils.StatsDateSelector import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider -import org.wordpress.android.util.config.StatsTrafficTabFeatureConfig import java.util.Date @ExperimentalCoroutinesApi @@ -29,8 +28,6 @@ class StatsDateSelectorTest : BaseUnitTest() { @Mock lateinit var siteProvider: StatsSiteProvider - @Mock - lateinit var statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig private val selectedDate = Date(0) private val selectedDateLabel = "Jan 1" private val statsGranularity = StatsGranularity.DAYS @@ -52,13 +49,11 @@ class StatsDateSelectorTest : BaseUnitTest() { statsDateFormatter, siteProvider, statsGranularity, - false, - statsTrafficTabFeatureConfig + false ) whenever(selectedDateProvider.getSelectedDate(statsGranularity)).thenReturn(selectedDate) whenever(statsDateFormatter.printGranularDate(selectedDate, statsGranularity)).thenReturn(selectedDateLabel) whenever(statsDateFormatter.printGranularDate(updatedDate, statsGranularity)).thenReturn(updatedLabel) - whenever(statsTrafficTabFeatureConfig.isEnabled()).thenReturn(true) } @Test diff --git a/build.gradle b/build.gradle index 54e736b12757..208be6e589f3 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ plugins { id "com.android.library" apply false id 'com.google.gms.google-services' apply false id "org.jetbrains.kotlin.plugin.parcelize" apply false + id "com.google.devtools.ksp" apply false } ext { @@ -22,9 +23,9 @@ ext { automatticAboutVersion = '1.4.0' automatticRestVersion = '1.0.8' automatticTracksVersion = '3.4.0' - gutenbergMobileVersion = 'v1.114.1' + gutenbergMobileVersion = 'v1.115.0-alpha3' wordPressAztecVersion = 'v2.0' - wordPressFluxCVersion = '2.70.1' + wordPressFluxCVersion = 'trunk-b9ecc708dde74d6cc95aeab42e56fb8067640039' wordPressLoginVersion = '1.14.1' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.13.0' @@ -42,7 +43,7 @@ ext { androidxArchCoreVersion = '2.2.0' androidxCameraVersion = '1.2.3' androidxComposeBomVersion = '2023.10.00' - androidxComposeCompilerVersion = '1.5.3' + androidxComposeCompilerVersion = '1.5.9' androidxComposeNavigationVersion = '2.7.6' androidxCardviewVersion = '1.0.0' androidxConstraintlayoutVersion = '2.1.4' @@ -91,7 +92,7 @@ ext { zendeskVersion = '5.1.2' // react native - facebookReactVersion = '0.71.15' + facebookReactVersion = '0.73.3' // test assertjVersion = '3.23.1' diff --git a/config/lint/lint.xml b/config/lint/lint.xml index e779334f1bba..3985b167379f 100644 --- a/config/lint/lint.xml +++ b/config/lint/lint.xml @@ -19,7 +19,7 @@ - + @@ -63,7 +63,7 @@ - + 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 d81d0abf3f59..5d3b930e26e2 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 @@ -35,7 +35,6 @@ import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnGutenbergDidRequestUnsupportedBlockFallbackListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnGutenbergDidSendButtonPressedActionListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnImageFullscreenPreviewListener; -import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnReattachMediaSavingQueryListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnReattachMediaUploadQueryListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnFocalPointPickerTooltipShownEventListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnMediaEditorListener; @@ -72,7 +71,6 @@ public boolean hasReceivedAnyContent() { public void attachToContainer(ViewGroup viewGroup, OnMediaLibraryButtonListener onMediaLibraryButtonListener, OnReattachMediaUploadQueryListener onReattachQueryListener, - OnReattachMediaSavingQueryListener onStorySavingReattachQueryListener, OnSetFeaturedImageListener onSetFeaturedImageListener, OnEditorMountListener onEditorMountListener, OnEditorAutosaveListener onEditorAutosaveListener, @@ -102,7 +100,6 @@ public void attachToContainer(ViewGroup viewGroup, OnMediaLibraryButtonListener viewGroup, onMediaLibraryButtonListener, onReattachQueryListener, - onStorySavingReattachQueryListener, onSetFeaturedImageListener, onEditorMountListener, onEditorAutosaveListener, @@ -114,7 +111,6 @@ public void attachToContainer(ViewGroup viewGroup, OnMediaLibraryButtonListener onGutenbergDidRequestEmbedFullscreenPreviewListener, onGutenbergDidSendButtonPressedActionListener, showSuggestionsUtil, - null, onFPPTooltipShownEventListener, onGutenbergDidRequestPreviewListener, onBlockTypeImpressionsListener, 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 1426d6a6f382..29199a679fb8 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 @@ -81,7 +81,6 @@ import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnGutenbergDidRequestUnsupportedBlockFallbackListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnGutenbergDidSendButtonPressedActionListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnMediaLibraryButtonListener; -import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnReattachMediaSavingQueryListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnReattachMediaUploadQueryListener; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnSetFeaturedImageListener; @@ -362,14 +361,6 @@ public void onQueryCurrentProgressForUploadingMedia() { updateMediaProgress(); } }, - new OnReattachMediaSavingQueryListener() { - @Override public void onQueryCurrentProgressForSavingMedia() { - // TODO: probably go through mFailedMediaIds, and see if any block in the post content - // has these mediaFIleIds. If there's a match, mark such a block in FAILED state. - updateFailedMediaState(); - updateMediaProgress(); - } - }, new OnSetFeaturedImageListener() { @Override public void onSetFeaturedImageButtonClicked(int mediaId) { diff --git a/libs/mocks/src/main/assets/mocks/mappings/wpcom/reader/rest_v2_read_streams_discover.json b/libs/mocks/src/main/assets/mocks/mappings/wpcom/reader/rest_v2_read_streams_discover.json new file mode 100644 index 000000000000..f12c5ad06505 --- /dev/null +++ b/libs/mocks/src/main/assets/mocks/mappings/wpcom/reader/rest_v2_read_streams_discover.json @@ -0,0 +1,3859 @@ +{ + "request": { + "method": "GET", + "urlPath": "/wpcom/v2/read/streams/discover" + }, + "response": { + "status": 200, + "jsonBody": { + "success": true, + "tags": ["photography"], + "sort": "popularity", + "lang": "en", + "page": 1, + "refresh": 1, + "cards": [{ + "type": "interests_you_may_like", + "data": [{ + "slug": "blogging", + "title": "Blogging", + "score": 278 + }, { + "slug": "travel", + "title": "Travel", + "score": 251 + }, { + "slug": "photos", + "title": "Photos", + "score": 173 + }, { + "slug": "technology", + "title": "Technology", + "score": 139 + }] + }, { + "type": "post", + "data": { + "ID": 37222, + "site_ID": 53424024, + "author": { + "ID": 47411601, + "login": "benhuberman", + "email": false, + "name": "Ben Huberman", + "first_name": "Ben", + "last_name": "Huberman", + "nice_name": "benhuberman", + "URL": "https://benz.blog/", + "avatar_URL": "https://0.gravatar.com/avatar/663dcd498e8c5f255bfb230a7ba07678?s=96&d=identicon&r=G", + "profile_URL": "http://en.gravatar.com/benhuberman", + "site_ID": 122910962, + "has_avatar": true + }, + "date": "{{now offset='-2 hours'}}", + "modified": "{{now offset='-2 hours'}}", + "title": "Kelsey Montague Art", + "URL": "https://discover.wordpress.com/2019/05/27/kelsey-montague-art/", + "short_URL": "https://wp.me/p3Ca1O-9Gm", + "content": "

Explore mural artist Kelsey Montague’s work, stay up-to-date on her latest projects, and shop for prints on her Anything Is Possible-featured website. 

\r\n\r\n\r\n

\r\n", + "excerpt": "

Explore mural artist Kelsey Montague’s work, stay up-to-date on her latest projects, and shop for prints on her Anything Is Possible-featured website. 

\n", + "slug": "kelsey-montague-art", + "guid": "https://discover.wordpress.com/?p=37222", + "status": "publish", + "sticky": false, + "password": "", + "parent": false, + "type": "post", + "discussion": { + "comments_open": false, + "comment_status": "closed", + "pings_open": false, + "ping_status": "closed", + "comment_count": 0 + }, + "likes_enabled": true, + "sharing_enabled": true, + "like_count": 33, + "i_like": false, + "is_reblogged": false, + "is_following": true, + "global_ID": "b06fd6b4fd1b95644d71981f34f747f8", + "featured_image": "https://discover.files.wordpress.com/2019/05/img_3760.jpg", + "post_thumbnail": { + "ID": 37226, + "URL": "https://discover.files.wordpress.com/2019/05/img_3760.jpg", + "guid": "http://discover.files.wordpress.com/2019/05/img_3760.jpg", + "mime_type": "image/jpeg", + "width": 1280, + "height": 1706 + }, + "format": "standard", + "geo": false, + "menu_order": 0, + "page_template": "", + "publicize_URLs": [], + "terms": { + "category": { + "Art": { + "ID": 177, + "name": "Art", + "slug": "art", + "description": "Artists' sites, powerful artwork in multiple genres in a variety of mediums, and news from the art world.", + "post_count": 370, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:art", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:art/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Culture": { + "ID": 1098, + "name": "Culture", + "slug": "culture", + "description": "A curated collection of WordPress sites on society, culture in all its forms, and diverse art forms from around the world.", + "post_count": 414, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:culture", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:culture/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Diversity": { + "ID": 47458, + "name": "Diversity", + "slug": "diversity", + "description": "Blogs, stories, and web projects that highlight activists working towards greater diversity in culture, politics, and the business world.", + "post_count": 131, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:diversity", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:diversity/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Feminism": { + "ID": 553, + "name": "Feminism", + "slug": "feminism", + "description": "Magazines, collaborative websites, and feminist blogs that cover important issues in politics, culture, diversity, and the fight for gender equality.", + "post_count": 99, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:feminism", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:feminism/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Inspiration": { + "ID": 107, + "name": "Inspiration", + "slug": "inspiration", + "description": "Ideas and advice to empower and motivate people – especially bloggers, writers, and creative types – in their personal or professional journeys.", + "post_count": 327, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:inspiration", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:inspiration/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Popular Culture": { + "ID": 2437, + "name": "Popular Culture", + "slug": "popular-culture", + "description": "The web's leading pop culture magazines and blogs, covering music, film, television, gaming, and more.", + "post_count": 198, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:popular-culture", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:popular-culture/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "post_tag": { + "AnythingIsPossible": { + "ID": 109479402, + "name": "AnythingIsPossible", + "slug": "anythingispossible", + "description": "", + "post_count": 15, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:anythingispossible", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:anythingispossible/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "mural": { + "ID": 159763, + "name": "mural", + "slug": "mural", + "description": "", + "post_count": 4, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:mural", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:mural/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "mural art": { + "ID": 5762997, + "name": "mural art", + "slug": "mural-art", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:mural-art", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:mural-art/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "murals": { + "ID": 135373, + "name": "murals", + "slug": "murals", + "description": "", + "post_count": 2, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:murals", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:murals/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "street art": { + "ID": 57447, + "name": "street art", + "slug": "street-art", + "description": "", + "post_count": 18, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:street-art", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:street-art/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "urban art": { + "ID": 28923, + "name": "urban art", + "slug": "urban-art", + "description": "", + "post_count": 2, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:urban-art", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:urban-art/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "post_format": {}, + "mentions": {} + }, + "tags": { + "AnythingIsPossible": { + "ID": 109479402, + "name": "AnythingIsPossible", + "slug": "anythingispossible", + "description": "", + "post_count": 15, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:anythingispossible", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:anythingispossible/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "anythingispossible" + }, + "mural": { + "ID": 159763, + "name": "mural", + "slug": "mural", + "description": "", + "post_count": 4, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:mural", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:mural/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "mural" + }, + "mural art": { + "ID": 5762997, + "name": "mural art", + "slug": "mural-art", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:mural-art", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:mural-art/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "mural-art" + }, + "murals": { + "ID": 135373, + "name": "murals", + "slug": "murals", + "description": "", + "post_count": 2, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:murals", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:murals/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "murals" + }, + "street art": { + "ID": 57447, + "name": "street art", + "slug": "street-art", + "description": "", + "post_count": 18, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:street-art", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:street-art/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "street-art" + }, + "urban art": { + "ID": 28923, + "name": "urban art", + "slug": "urban-art", + "description": "", + "post_count": 2, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:urban-art", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:urban-art/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "urban-art" + } + }, + "categories": { + "Art": { + "ID": 177, + "name": "Art", + "slug": "art", + "description": "Artists' sites, powerful artwork in multiple genres in a variety of mediums, and news from the art world.", + "post_count": 370, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:art", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:art/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Culture": { + "ID": 1098, + "name": "Culture", + "slug": "culture", + "description": "A curated collection of WordPress sites on society, culture in all its forms, and diverse art forms from around the world.", + "post_count": 414, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:culture", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:culture/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Diversity": { + "ID": 47458, + "name": "Diversity", + "slug": "diversity", + "description": "Blogs, stories, and web projects that highlight activists working towards greater diversity in culture, politics, and the business world.", + "post_count": 131, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:diversity", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:diversity/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Feminism": { + "ID": 553, + "name": "Feminism", + "slug": "feminism", + "description": "Magazines, collaborative websites, and feminist blogs that cover important issues in politics, culture, diversity, and the fight for gender equality.", + "post_count": 99, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:feminism", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:feminism/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Inspiration": { + "ID": 107, + "name": "Inspiration", + "slug": "inspiration", + "description": "Ideas and advice to empower and motivate people – especially bloggers, writers, and creative types – in their personal or professional journeys.", + "post_count": 327, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:inspiration", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:inspiration/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Popular Culture": { + "ID": 2437, + "name": "Popular Culture", + "slug": "popular-culture", + "description": "The web's leading pop culture magazines and blogs, covering music, film, television, gaming, and more.", + "post_count": 198, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:popular-culture", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:popular-culture/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "attachments": { + "37226": { + "ID": 37226, + "URL": "https://discover.files.wordpress.com/2019/05/img_3760.jpg", + "guid": "http://discover.files.wordpress.com/2019/05/img_3760.jpg", + "date": "2019-05-21T23:48:08-04:00", + "post_ID": 37222, + "author_ID": 47411601, + "file": "img_3760.jpg", + "mime_type": "image/jpeg", + "extension": "jpg", + "title": "IMG_3760", + "caption": "", + "description": "", + "alt": "Kelsey Montague mural art", + "thumbnails": { + "thumbnail": "https://discover.files.wordpress.com/2019/05/img_3760.jpg?w=113", + "medium": "https://discover.files.wordpress.com/2019/05/img_3760.jpg?w=311", + "large": "https://discover.files.wordpress.com/2019/05/img_3760.jpg?w=768" + }, + "height": 1706, + "width": 1280, + "exif": { + "aperture": "1.8", + "credit": "", + "camera": "iPhone 8 Plus", + "caption": "", + "created_timestamp": "1536421883", + "copyright": "", + "focal_length": "3.99", + "iso": "20", + "shutter_speed": "0.0014836795252226", + "title": "", + "orientation": "0", + "keywords": [] + }, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/media/37226", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/media/37226/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "parent": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37222" + } + } + } + }, + "attachment_count": 1, + "metadata": [ + { + "id": "130242", + "key": "geo_public", + "value": "0" + }, + { + "id": "130230", + "key": "_thumbnail_id", + "value": "37226" + }, + { + "id": "130401", + "key": "_wpas_done_17927786", + "value": "1" + }, + { + "id": "130238", + "key": "_wpas_mess", + "value": "Visit @kelsmontagueart's website - featured on @wordpressdotcom's #AnythingIsPossible list - for inspiration, prints, and updates on Kelsey's latest mural projects." + } + ], + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/read/sites/53424024/posts/37222", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/posts/37222/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "replies": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37222/replies/", + "likes": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37222/likes/" + }, + "data": { + "site": { + "ID": 53424024, + "name": "Discover", + "description": "A daily selection of the best content published on WordPress, collected for you by humans who love to read.", + "URL": "https://discover.wordpress.com", + "jetpack": false, + "post_count": 3603, + "subscribers_count": 35192480, + "locale": "en", + "icon": { + "img": "https://secure.gravatar.com/blavatar/c9e4e04719c81ca4936a63ea2dce6ace", + "ico": "https://secure.gravatar.com/blavatar/c9e4e04719c81ca4936a63ea2dce6ace" + }, + "logo": { + "id": 0, + "sizes": [], + "url": "" + }, + "visible": true, + "is_private": false, + "is_following": true, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/help", + "posts": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/posts/", + "comments": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/comments/", + "xmlrpc": "https://discover.wordpress.com/xmlrpc.php" + } + } + } + } + }, + "capabilities": { + "publish_post": false, + "delete_post": false, + "edit_post": false + }, + "other_URLs": {}, + "discover_metadata": { + "permalink": "https://kelseymontagueart.com/", + "attribution": { + "author_name": "Krista Stevens", + "author_url": "https://kelseymontagueart.com/", + "blog_name": "Kelsey Montague Art", + "blog_url": "https://kelseymontagueart.com", + "avatar_url": "https://discover.files.wordpress.com/2019/05/kelsey-thumbnail.png?w=100&h=100&crop=true" + }, + "discover_fp_post_formats": [ + { + "name": "Pick", + "slug": "pick", + "id": 346750 + }, + { + "name": "Site Pick", + "slug": "site-pick", + "id": 308219249 + } + ], + "featured_post_wpcom_data": { + "blog_id": 161169196 + } + }, + "feed_ID": 41325786, + "feed_URL": "http://discover.wordpress.com", + "pseudo_ID": "b06fd6b4fd1b95644d71981f34f747f8", + "is_external": false, + "site_name": "Discover", + "site_URL": "https://discover.wordpress.com", + "site_is_private": false, + "featured_media": {}, + "use_excerpt": false, + "is_following_conversation": false + } + }, { + "type": "post", + "data": { + "ID": 37189, + "site_ID": 53424024, + "author": { + "ID": 10183950, + "login": "cherilucas", + "email": false, + "name": "Cheri Lucas Rowlands", + "first_name": "Cheri", + "last_name": "Rowlands", + "nice_name": "cherilucas", + "URL": "http://cherilucasrowlands.com", + "avatar_URL": "https://0.gravatar.com/avatar/36207d4c7c014b0999b995ca3971d383?s=96&d=identicon&r=G", + "profile_URL": "http://en.gravatar.com/cherilucas", + "site_ID": 9838404, + "has_avatar": true + }, + "date": "{{now offset='-1 days'}}", + "modified": "{{now offset='-1 days'}}", + "title": "The Radical Notion of Not Letting Work Define You", + "URL": "https://discover.wordpress.com/2019/05/26/the-radical-notion-of-not-letting-work-define-you/", + "short_URL": "https://wp.me/p3Ca1O-9FP", + "content": "

“Just because something can’t be a career doesn’t have to mean that it can’t be part of your life and identity.” At Man Repeller, Molly Conway muses on imposter syndrome, work and identity, and being a playwright.

\n", + "excerpt": "

“Just because something can’t be a career doesn’t have to mean that it can’t be part of your life and identity.” At Man Repeller, Molly Conway muses on imposter syndrome, work and identity, and being a playwright.

\n", + "slug": "the-radical-notion-of-not-letting-work-define-you", + "guid": "https://discover.wordpress.com/?p=37189", + "status": "publish", + "sticky": false, + "password": "", + "parent": false, + "type": "post", + "discussion": { + "comments_open": false, + "comment_status": "closed", + "pings_open": false, + "ping_status": "closed", + "comment_count": 0 + }, + "likes_enabled": true, + "sharing_enabled": true, + "like_count": 102, + "i_like": false, + "is_reblogged": false, + "is_following": true, + "global_ID": "eedec98542f8cbec7df4d4c608c847ef", + "featured_image": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-20-at-11.50.07-am.png", + "post_thumbnail": { + "ID": 37191, + "URL": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-20-at-11.50.07-am.png", + "guid": "http://discover.files.wordpress.com/2019/05/screen-shot-2019-05-20-at-11.50.07-am.png", + "mime_type": "image/png", + "width": 1482, + "height": 988 + }, + "format": "standard", + "geo": false, + "menu_order": 0, + "page_template": "", + "publicize_URLs": [], + "terms": { + "category": { + "Identity": { + "ID": 10679, + "name": "Identity", + "slug": "identity", + "description": "Engaging conversations and writing around the topics of identity, diversity, and the search for authenticity.", + "post_count": 166, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:identity", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:identity/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Personal Essay": { + "ID": 253221, + "name": "Personal Essay", + "slug": "personal-essay", + "description": "Personal and introspective essays and longform from new and established writers and authors.", + "post_count": 156, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:personal-essay", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:personal-essay/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Personal Musings": { + "ID": 5316, + "name": "Personal Musings", + "slug": "personal-musings", + "description": "Introspective and self-reflective writing in various formats.", + "post_count": 437, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:personal-musings", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:personal-musings/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Work": { + "ID": 131, + "name": "Work", + "slug": "work", + "description": "From tech to farming, posts and websites dedicated to the many facets of work, work-life balance, and social inequality.", + "post_count": 80, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:work", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:work/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Writing": { + "ID": 349, + "name": "Writing", + "slug": "writing", + "description": "Writing, advice, and commentary on the act and process of writing, blogging, and publishing.", + "post_count": 437, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:writing", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:writing/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "post_tag": { + "imposter syndrome": { + "ID": 392126, + "name": "imposter syndrome", + "slug": "imposter-syndrome", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:imposter-syndrome", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:imposter-syndrome/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "playwright": { + "ID": 160393, + "name": "playwright", + "slug": "playwright", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:playwright", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:playwright/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "PoweredByWordPress": { + "ID": 76162589, + "name": "PoweredByWordPress", + "slug": "poweredbywordpress", + "description": "", + "post_count": 42, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:poweredbywordpress", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:poweredbywordpress/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "writers": { + "ID": 16761, + "name": "writers", + "slug": "writers", + "description": "", + "post_count": 70, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:writers", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:writers/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "post_format": {}, + "mentions": {} + }, + "tags": { + "imposter syndrome": { + "ID": 392126, + "name": "imposter syndrome", + "slug": "imposter-syndrome", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:imposter-syndrome", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:imposter-syndrome/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "imposter-syndrome" + }, + "playwright": { + "ID": 160393, + "name": "playwright", + "slug": "playwright", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:playwright", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:playwright/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "playwright" + }, + "PoweredByWordPress": { + "ID": 76162589, + "name": "PoweredByWordPress", + "slug": "poweredbywordpress", + "description": "", + "post_count": 42, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:poweredbywordpress", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:poweredbywordpress/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "poweredbywordpress" + }, + "writers": { + "ID": 16761, + "name": "writers", + "slug": "writers", + "description": "", + "post_count": 70, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:writers", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:writers/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "writers" + } + }, + "categories": { + "Identity": { + "ID": 10679, + "name": "Identity", + "slug": "identity", + "description": "Engaging conversations and writing around the topics of identity, diversity, and the search for authenticity.", + "post_count": 166, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:identity", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:identity/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Personal Essay": { + "ID": 253221, + "name": "Personal Essay", + "slug": "personal-essay", + "description": "Personal and introspective essays and longform from new and established writers and authors.", + "post_count": 156, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:personal-essay", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:personal-essay/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Personal Musings": { + "ID": 5316, + "name": "Personal Musings", + "slug": "personal-musings", + "description": "Introspective and self-reflective writing in various formats.", + "post_count": 437, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:personal-musings", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:personal-musings/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Work": { + "ID": 131, + "name": "Work", + "slug": "work", + "description": "From tech to farming, posts and websites dedicated to the many facets of work, work-life balance, and social inequality.", + "post_count": 80, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:work", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:work/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Writing": { + "ID": 349, + "name": "Writing", + "slug": "writing", + "description": "Writing, advice, and commentary on the act and process of writing, blogging, and publishing.", + "post_count": 437, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:writing", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:writing/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "attachments": { + "37191": { + "ID": 37191, + "URL": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-20-at-11.50.07-am.png", + "guid": "http://discover.files.wordpress.com/2019/05/screen-shot-2019-05-20-at-11.50.07-am.png", + "date": "2019-05-20T14:50:23-04:00", + "post_ID": 37189, + "author_ID": 10183950, + "file": "screen-shot-2019-05-20-at-11.50.07-am.png", + "mime_type": "image/png", + "extension": "png", + "title": "man repeller header image", + "caption": "", + "description": "", + "alt": "", + "thumbnails": { + "thumbnail": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-20-at-11.50.07-am.png?w=150", + "medium": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-20-at-11.50.07-am.png?w=315", + "large": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-20-at-11.50.07-am.png?w=1220" + }, + "height": 988, + "width": 1482, + "exif": { + "aperture": "0", + "credit": "", + "camera": "", + "caption": "", + "created_timestamp": "0", + "copyright": "", + "focal_length": "0", + "iso": "0", + "shutter_speed": "0", + "title": "", + "orientation": "0", + "keywords": [] + }, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/media/37191", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/media/37191/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "parent": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37189" + } + } + }, + "37192": { + "ID": 37192, + "URL": "https://discover.files.wordpress.com/2019/05/man-repeller-logo.jpg", + "guid": "http://discover.files.wordpress.com/2019/05/man-repeller-logo.jpg", + "date": "2019-05-20T14:50:47-04:00", + "post_ID": 37189, + "author_ID": 10183950, + "file": "man-repeller-logo.jpg", + "mime_type": "image/jpeg", + "extension": "jpg", + "title": "man repeller logo", + "caption": "", + "description": "", + "alt": "", + "thumbnails": { + "thumbnail": "https://discover.files.wordpress.com/2019/05/man-repeller-logo.jpg?w=150", + "medium": "https://discover.files.wordpress.com/2019/05/man-repeller-logo.jpg?w=315", + "large": "https://discover.files.wordpress.com/2019/05/man-repeller-logo.jpg?w=400" + }, + "height": 400, + "width": 400, + "exif": { + "aperture": "0", + "credit": "", + "camera": "", + "caption": "", + "created_timestamp": "0", + "copyright": "", + "focal_length": "0", + "iso": "0", + "shutter_speed": "0", + "title": "", + "orientation": "0", + "keywords": [] + }, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/media/37192", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/media/37192/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "parent": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37189" + } + } + } + }, + "attachment_count": 2, + "metadata": [ + { + "id": "130145", + "key": "geo_public", + "value": "0" + }, + { + "id": "130142", + "key": "_thumbnail_id", + "value": "37191" + }, + { + "id": "130392", + "key": "_wpas_done_17926349", + "value": "1" + }, + { + "id": "130146", + "key": "_wpas_mess", + "value": "\"Just because something can’t be a career doesn’t have to mean that it can’t be part of your life and identity.\" Molly Conway muses on imposter syndrome, work and identity, and being a playwright. (@ManRepeller)" + } + ], + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/read/sites/53424024/posts/37189", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/posts/37189/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "replies": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37189/replies/", + "likes": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37189/likes/" + }, + "data": { + "site": { + "ID": 53424024, + "name": "Discover", + "description": "A daily selection of the best content published on WordPress, collected for you by humans who love to read.", + "URL": "https://discover.wordpress.com", + "jetpack": false, + "post_count": 3603, + "subscribers_count": 35192480, + "locale": "en", + "icon": { + "img": "https://secure.gravatar.com/blavatar/c9e4e04719c81ca4936a63ea2dce6ace", + "ico": "https://secure.gravatar.com/blavatar/c9e4e04719c81ca4936a63ea2dce6ace" + }, + "logo": { + "id": 0, + "sizes": [], + "url": "" + }, + "visible": true, + "is_private": false, + "is_following": true, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/help", + "posts": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/posts/", + "comments": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/comments/", + "xmlrpc": "https://discover.wordpress.com/xmlrpc.php" + } + } + } + } + }, + "capabilities": { + "publish_post": false, + "delete_post": false, + "edit_post": false + }, + "other_URLs": {}, + "discover_metadata": { + "permalink": "https://www.manrepeller.com/2019/05/work-identity.html", + "attribution": { + "author_name": "Molly Conway", + "author_url": "https://www.manrepeller.com/author/molly-conway", + "blog_name": "Man Repeller", + "blog_url": "https://www.manrepeller.com", + "avatar_url": "https://discover.files.wordpress.com/2019/05/man-repeller-logo.jpg?w=100&h=100&crop=true" + }, + "discover_fp_post_formats": [ + { + "name": "Pick", + "slug": "pick", + "id": 346750 + }, + { + "name": "Standard Pick", + "slug": "standard-pick", + "id": 337879995 + } + ], + "featured_post_wpcom_data": { + "blog_id": 61780023 + } + }, + "feed_ID": 41325786, + "feed_URL": "http://discover.wordpress.com", + "pseudo_ID": "eedec98542f8cbec7df4d4c608c847ef", + "is_external": false, + "site_name": "Discover", + "site_URL": "https://discover.wordpress.com", + "site_is_private": false, + "featured_media": { + "uri": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-20-at-11.50.07-am.png", + "width": 1482, + "height": 988, + "type": "image" + }, + "use_excerpt": false, + "is_following_conversation": false + } + }, { + "type": "post", + "data": { + "ID": 37205, + "site_ID": 53424024, + "author": { + "ID": 47411601, + "login": "benhuberman", + "email": false, + "name": "Ben Huberman", + "first_name": "Ben", + "last_name": "Huberman", + "nice_name": "benhuberman", + "URL": "https://benz.blog/", + "avatar_URL": "https://0.gravatar.com/avatar/663dcd498e8c5f255bfb230a7ba07678?s=96&d=identicon&r=G", + "profile_URL": "http://en.gravatar.com/benhuberman", + "site_ID": 122910962, + "has_avatar": true + }, + "date": "2019-05-25T09:00:36-04:00", + "modified": "2019-05-21T23:45:15-04:00", + "title": "Barista Hustle", + "URL": "https://discover.wordpress.com/2019/05/25/barista-hustle/", + "short_URL": "https://wp.me/p3Ca1O-9G5", + "content": "\n

The team at Barista Hustle, a leading coffee-education hub, creates resources and shares their extensive knowledge on topics ranging from cutting-edge gear to latte art.

\n", + "excerpt": "

The team at Barista Hustle, a leading coffee-education hub, creates resources and shares their extensive knowledge on topics ranging from cutting-edge gear to latte art.

\n", + "slug": "barista-hustle", + "guid": "https://discover.wordpress.com/?p=37205", + "status": "publish", + "sticky": false, + "password": "", + "parent": false, + "type": "post", + "discussion": { + "comments_open": false, + "comment_status": "closed", + "pings_open": false, + "ping_status": "closed", + "comment_count": 0 + }, + "likes_enabled": true, + "sharing_enabled": true, + "like_count": 58, + "i_like": false, + "is_reblogged": false, + "is_following": true, + "global_ID": "5344123ea1ee2da1788f11183966d068", + "featured_image": "https://discover.files.wordpress.com/2019/05/bh-home-header-element-wide-final.jpg", + "post_thumbnail": { + "ID": 37206, + "URL": "https://discover.files.wordpress.com/2019/05/bh-home-header-element-wide-final.jpg", + "guid": "http://discover.files.wordpress.com/2019/05/bh-home-header-element-wide-final.jpg", + "mime_type": "image/jpeg", + "width": 2800, + "height": 1500 + }, + "format": "standard", + "geo": false, + "menu_order": 0, + "page_template": "", + "publicize_URLs": [], + "terms": { + "category": { + "Business": { + "ID": 179, + "name": "Business", + "slug": "business", + "description": "Small business and ecommerce resources, writing for professionals, and commentary on business, economics, and related topics.", + "post_count": 88, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:business", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:business/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Education": { + "ID": 1342, + "name": "Education", + "slug": "education", + "description": "Resources across disciplines and perspectives on teaching, learning, and the educational system from educators, teachers, and parents.", + "post_count": 121, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:education", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:education/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Food": { + "ID": 586, + "name": "Food", + "slug": "food", + "description": "Recipes, writing on food and culinary culture, and food photography or visual art.", + "post_count": 215, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:food", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:food/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Lifestyle": { + "ID": 278, + "name": "Lifestyle", + "slug": "lifestyle", + "description": "Sites devoted to fashion and beauty, interior design, travel, alternative ways of living, and more.", + "post_count": 127, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:lifestyle", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:lifestyle/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Work": { + "ID": 131, + "name": "Work", + "slug": "work", + "description": "From tech to farming, posts and websites dedicated to the many facets of work, work-life balance, and social inequality.", + "post_count": 80, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:work", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:work/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "post_tag": { + "Baristas": { + "ID": 831444, + "name": "Baristas", + "slug": "baristas", + "description": "", + "post_count": 2, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:baristas", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:baristas/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "business blog": { + "ID": 287435, + "name": "business blog", + "slug": "business-blog", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:business-blog", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:business-blog/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "coffee": { + "ID": 16166, + "name": "coffee", + "slug": "coffee", + "description": "", + "post_count": 10, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:coffee", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:coffee/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "ecommerce": { + "ID": 11160, + "name": "ecommerce", + "slug": "ecommerce", + "description": "", + "post_count": 10, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:ecommerce", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:ecommerce/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Small Business": { + "ID": 10585, + "name": "Small Business", + "slug": "small-business", + "description": "", + "post_count": 37, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:small-business", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:small-business/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "post_format": {}, + "mentions": {} + }, + "tags": { + "Baristas": { + "ID": 831444, + "name": "Baristas", + "slug": "baristas", + "description": "", + "post_count": 2, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:baristas", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:baristas/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "baristas" + }, + "business blog": { + "ID": 287435, + "name": "business blog", + "slug": "business-blog", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:business-blog", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:business-blog/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "business-blog" + }, + "coffee": { + "ID": 16166, + "name": "coffee", + "slug": "coffee", + "description": "", + "post_count": 10, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:coffee", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:coffee/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "coffee" + }, + "ecommerce": { + "ID": 11160, + "name": "ecommerce", + "slug": "ecommerce", + "description": "", + "post_count": 10, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:ecommerce", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:ecommerce/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "ecommerce" + }, + "Small Business": { + "ID": 10585, + "name": "Small Business", + "slug": "small-business", + "description": "", + "post_count": 37, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:small-business", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:small-business/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "small-business" + } + }, + "categories": { + "Business": { + "ID": 179, + "name": "Business", + "slug": "business", + "description": "Small business and ecommerce resources, writing for professionals, and commentary on business, economics, and related topics.", + "post_count": 88, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:business", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:business/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Education": { + "ID": 1342, + "name": "Education", + "slug": "education", + "description": "Resources across disciplines and perspectives on teaching, learning, and the educational system from educators, teachers, and parents.", + "post_count": 121, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:education", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:education/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Food": { + "ID": 586, + "name": "Food", + "slug": "food", + "description": "Recipes, writing on food and culinary culture, and food photography or visual art.", + "post_count": 215, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:food", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:food/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Lifestyle": { + "ID": 278, + "name": "Lifestyle", + "slug": "lifestyle", + "description": "Sites devoted to fashion and beauty, interior design, travel, alternative ways of living, and more.", + "post_count": 127, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:lifestyle", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:lifestyle/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Work": { + "ID": 131, + "name": "Work", + "slug": "work", + "description": "From tech to farming, posts and websites dedicated to the many facets of work, work-life balance, and social inequality.", + "post_count": 80, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:work", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:work/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "attachments": {}, + "attachment_count": 0, + "metadata": [ + { + "id": "130216", + "key": "geo_public", + "value": "0" + }, + { + "id": "130206", + "key": "_thumbnail_id", + "value": "37206" + }, + { + "id": "130379", + "key": "_wpas_done_17927786", + "value": "1" + }, + { + "id": "130212", + "key": "_wpas_mess", + "value": "Whether you're a coffee professional, an aspiring latte artist, or just looking to improve your next cup, check out the resources and courses @BaristaHustle, a #PoweredByWordPress website:" + } + ], + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/read/sites/53424024/posts/37205", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/posts/37205/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "replies": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37205/replies/", + "likes": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37205/likes/" + }, + "data": { + "site": { + "ID": 53424024, + "name": "Discover", + "description": "A daily selection of the best content published on WordPress, collected for you by humans who love to read.", + "URL": "https://discover.wordpress.com", + "jetpack": false, + "post_count": 3603, + "subscribers_count": 35192480, + "locale": "en", + "icon": { + "img": "https://secure.gravatar.com/blavatar/c9e4e04719c81ca4936a63ea2dce6ace", + "ico": "https://secure.gravatar.com/blavatar/c9e4e04719c81ca4936a63ea2dce6ace" + }, + "logo": { + "id": 0, + "sizes": [], + "url": "" + }, + "visible": true, + "is_private": false, + "is_following": true, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/help", + "posts": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/posts/", + "comments": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/comments/", + "xmlrpc": "https://discover.wordpress.com/xmlrpc.php" + } + } + } + } + }, + "capabilities": { + "publish_post": false, + "delete_post": false, + "edit_post": false + }, + "other_URLs": {}, + "discover_metadata": { + "permalink": "https://baristahustle.com/", + "attribution": { + "author_name": "Krista Stevens", + "author_url": "https://baristahustle.com/", + "blog_name": "Barista Hustle", + "blog_url": "https://baristahustle.com", + "avatar_url": "https://discover.files.wordpress.com/2019/05/bh-logo-new-512-1-400x400.png?w=100&h=100&crop=true" + }, + "discover_fp_post_formats": [ + { + "name": "Pick", + "slug": "pick", + "id": 346750 + }, + { + "name": "Site Pick", + "slug": "site-pick", + "id": 308219249 + } + ], + "featured_post_wpcom_data": { + "blog_id": 82609915 + } + }, + "feed_ID": 41325786, + "feed_URL": "http://discover.wordpress.com", + "pseudo_ID": "5344123ea1ee2da1788f11183966d068", + "is_external": false, + "site_name": "Discover", + "site_URL": "https://discover.wordpress.com", + "site_is_private": false, + "featured_media": {}, + "use_excerpt": false, + "is_following_conversation": false + } + }, { + "type": "post", + "data": { + "ID": 37123, + "site_ID": 53424024, + "author": { + "ID": 10183950, + "login": "cherilucas", + "email": false, + "name": "Cheri Lucas Rowlands", + "first_name": "Cheri", + "last_name": "Rowlands", + "nice_name": "cherilucas", + "URL": "http://cherilucasrowlands.com", + "avatar_URL": "https://0.gravatar.com/avatar/36207d4c7c014b0999b995ca3971d383?s=96&d=identicon&r=G", + "profile_URL": "http://en.gravatar.com/cherilucas", + "site_ID": 9838404, + "has_avatar": true + }, + "date": "2019-05-24T09:00:50-04:00", + "modified": "2019-05-22T17:07:30-04:00", + "title": "Lonely Planet Kids", + "URL": "https://discover.wordpress.com/2019/05/24/lonely-planet-kids/", + "short_URL": "https://wp.me/p3Ca1O-9EL", + "content": "

Lonely Planet Kids — an offshoot of the popular travel guide company –inspires children to be curious about the world. The site features books, activities, family travel posts, and more.

\n", + "excerpt": "

Lonely Planet Kids — an offshoot of the popular travel guide company –inspires children to be curious about the world. The site features books, activities, family travel posts, and more.

\n", + "slug": "lonely-planet-kids", + "guid": "https://discover.wordpress.com/?p=37123", + "status": "publish", + "sticky": false, + "password": "", + "parent": false, + "type": "post", + "discussion": { + "comments_open": false, + "comment_status": "closed", + "pings_open": false, + "ping_status": "closed", + "comment_count": 0 + }, + "likes_enabled": true, + "sharing_enabled": true, + "like_count": 49, + "i_like": false, + "is_reblogged": false, + "is_following": true, + "global_ID": "12ac818a3de41e3b0cf84fea7efa2592", + "featured_image": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-10-at-3.53.09-pm.png", + "post_thumbnail": { + "ID": 37125, + "URL": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-10-at-3.53.09-pm.png", + "guid": "http://discover.files.wordpress.com/2019/05/screen-shot-2019-05-10-at-3.53.09-pm.png", + "mime_type": "image/png", + "width": 1434, + "height": 808 + }, + "format": "standard", + "geo": false, + "menu_order": 0, + "page_template": "", + "publicize_URLs": [], + "terms": { + "category": { + "Books": { + "ID": 178, + "name": "Books", + "slug": "books", + "description": "Writing, reviews, resources, and news on books, authors, reading, and publishing.", + "post_count": 233, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:books", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:books/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Education": { + "ID": 1342, + "name": "Education", + "slug": "education", + "description": "Resources across disciplines and perspectives on teaching, learning, and the educational system from educators, teachers, and parents.", + "post_count": 121, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:education", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:education/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Exploration": { + "ID": 7543, + "name": "Exploration", + "slug": "exploration", + "description": "Writing and photography on travel, self-discovery, research, observation, and more.", + "post_count": 147, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:exploration", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:exploration/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Family": { + "ID": 406, + "name": "Family", + "slug": "family", + "description": "Writing that encompasses aspects of family, including marriage, parenting, childhood, relationships, and ancestry.", + "post_count": 226, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:family", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:family/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Inspiration": { + "ID": 107, + "name": "Inspiration", + "slug": "inspiration", + "description": "Ideas and advice to empower and motivate people – especially bloggers, writers, and creative types – in their personal or professional journeys.", + "post_count": 327, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:inspiration", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:inspiration/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Parenting": { + "ID": 5309, + "name": "Parenting", + "slug": "parenting", + "description": "Writing and resources on parenting, motherhood, marriage, and family.", + "post_count": 141, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:parenting", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:parenting/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Publishing": { + "ID": 3330, + "name": "Publishing", + "slug": "publishing", + "description": "Writers and editors discussing publishing-industry news, the ins and outs of the literary world, and their own journey to a published book.", + "post_count": 124, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:publishing", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:publishing/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Reading": { + "ID": 1473, + "name": "Reading", + "slug": "reading", + "description": "Posts and book blogs that focus on authors, book reviews, and the pleasures of reading in the digital age.", + "post_count": 143, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:reading", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:reading/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Travel": { + "ID": 200, + "name": "Travel", + "slug": "travel", + "description": "Blogs, online guides, and trip planning resources devoted to travel, exploration, the outdoors, expat life, and global culture.", + "post_count": 247, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:travel", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:travel/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Wanderlust": { + "ID": 13181, + "name": "Wanderlust", + "slug": "wanderlust", + "description": "Stunning travel photography, travel and lifestyle sites, and blog posts on the joys and rewards of exploring new destinations.", + "post_count": 97, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:wanderlust", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:wanderlust/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "post_tag": { + "activities": { + "ID": 6751, + "name": "activities", + "slug": "activities", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:activities", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:activities/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "children": { + "ID": 1343, + "name": "children", + "slug": "children", + "description": "", + "post_count": 28, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:children", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:children/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "family travel": { + "ID": 421426, + "name": "family travel", + "slug": "family-travel", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:family-travel", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:family-travel/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "imagination": { + "ID": 10906, + "name": "imagination", + "slug": "imagination", + "description": "", + "post_count": 7, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:imagination", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:imagination/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "kids": { + "ID": 3374, + "name": "kids", + "slug": "kids", + "description": "", + "post_count": 14, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:kids", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:kids/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Lonely Planet": { + "ID": 232853, + "name": "Lonely Planet", + "slug": "lonely-planet", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:lonely-planet", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:lonely-planet/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Teaching": { + "ID": 1591, + "name": "Teaching", + "slug": "teaching", + "description": "", + "post_count": 2, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:teaching", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:teaching/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "post_format": {}, + "mentions": {} + }, + "tags": { + "activities": { + "ID": 6751, + "name": "activities", + "slug": "activities", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:activities", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:activities/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "activities" + }, + "children": { + "ID": 1343, + "name": "children", + "slug": "children", + "description": "", + "post_count": 28, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:children", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:children/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "children" + }, + "family travel": { + "ID": 421426, + "name": "family travel", + "slug": "family-travel", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:family-travel", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:family-travel/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "family-travel" + }, + "imagination": { + "ID": 10906, + "name": "imagination", + "slug": "imagination", + "description": "", + "post_count": 7, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:imagination", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:imagination/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "imagination" + }, + "kids": { + "ID": 3374, + "name": "kids", + "slug": "kids", + "description": "", + "post_count": 14, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:kids", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:kids/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "kids" + }, + "Lonely Planet": { + "ID": 232853, + "name": "Lonely Planet", + "slug": "lonely-planet", + "description": "", + "post_count": 1, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:lonely-planet", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:lonely-planet/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "lonely-planet" + }, + "Teaching": { + "ID": 1591, + "name": "Teaching", + "slug": "teaching", + "description": "", + "post_count": 2, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/tags/slug:teaching", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/tags/slug:teaching/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + }, + "display_name": "teaching" + } + }, + "categories": { + "Books": { + "ID": 178, + "name": "Books", + "slug": "books", + "description": "Writing, reviews, resources, and news on books, authors, reading, and publishing.", + "post_count": 233, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:books", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:books/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Education": { + "ID": 1342, + "name": "Education", + "slug": "education", + "description": "Resources across disciplines and perspectives on teaching, learning, and the educational system from educators, teachers, and parents.", + "post_count": 121, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:education", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:education/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Exploration": { + "ID": 7543, + "name": "Exploration", + "slug": "exploration", + "description": "Writing and photography on travel, self-discovery, research, observation, and more.", + "post_count": 147, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:exploration", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:exploration/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Family": { + "ID": 406, + "name": "Family", + "slug": "family", + "description": "Writing that encompasses aspects of family, including marriage, parenting, childhood, relationships, and ancestry.", + "post_count": 226, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:family", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:family/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Inspiration": { + "ID": 107, + "name": "Inspiration", + "slug": "inspiration", + "description": "Ideas and advice to empower and motivate people – especially bloggers, writers, and creative types – in their personal or professional journeys.", + "post_count": 327, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:inspiration", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:inspiration/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Parenting": { + "ID": 5309, + "name": "Parenting", + "slug": "parenting", + "description": "Writing and resources on parenting, motherhood, marriage, and family.", + "post_count": 141, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:parenting", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:parenting/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Publishing": { + "ID": 3330, + "name": "Publishing", + "slug": "publishing", + "description": "Writers and editors discussing publishing-industry news, the ins and outs of the literary world, and their own journey to a published book.", + "post_count": 124, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:publishing", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:publishing/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Reading": { + "ID": 1473, + "name": "Reading", + "slug": "reading", + "description": "Posts and book blogs that focus on authors, book reviews, and the pleasures of reading in the digital age.", + "post_count": 143, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:reading", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:reading/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Travel": { + "ID": 200, + "name": "Travel", + "slug": "travel", + "description": "Blogs, online guides, and trip planning resources devoted to travel, exploration, the outdoors, expat life, and global culture.", + "post_count": 247, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:travel", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:travel/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + }, + "Wanderlust": { + "ID": 13181, + "name": "Wanderlust", + "slug": "wanderlust", + "description": "Stunning travel photography, travel and lifestyle sites, and blog posts on the joys and rewards of exploring new destinations.", + "post_count": 97, + "parent": 0, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/categories/slug:wanderlust", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/categories/slug:wanderlust/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024" + } + } + } + }, + "attachments": { + "37125": { + "ID": 37125, + "URL": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-10-at-3.53.09-pm.png", + "guid": "http://discover.files.wordpress.com/2019/05/screen-shot-2019-05-10-at-3.53.09-pm.png", + "date": "2019-05-10T18:53:26-04:00", + "post_ID": 37123, + "author_ID": 10183950, + "file": "screen-shot-2019-05-10-at-3.53.09-pm.png", + "mime_type": "image/png", + "extension": "png", + "title": "lonely planet kids header", + "caption": "", + "description": "", + "alt": "", + "thumbnails": { + "thumbnail": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-10-at-3.53.09-pm.png?w=150", + "medium": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-10-at-3.53.09-pm.png?w=315", + "large": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-10-at-3.53.09-pm.png?w=1220" + }, + "height": 808, + "width": 1434, + "exif": { + "aperture": "0", + "credit": "", + "camera": "", + "caption": "", + "created_timestamp": "0", + "copyright": "", + "focal_length": "0", + "iso": "0", + "shutter_speed": "0", + "title": "", + "orientation": "0", + "keywords": [] + }, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/media/37125", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/media/37125/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "parent": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37123" + } + } + }, + "37126": { + "ID": 37126, + "URL": "https://discover.files.wordpress.com/2019/05/lonely-planet-kids-logo.png", + "guid": "http://discover.files.wordpress.com/2019/05/lonely-planet-kids-logo.png", + "date": "2019-05-10T18:53:28-04:00", + "post_ID": 37123, + "author_ID": 10183950, + "file": "lonely-planet-kids-logo.png", + "mime_type": "image/png", + "extension": "png", + "title": "lonely planet kids logo", + "caption": "", + "description": "", + "alt": "", + "thumbnails": { + "thumbnail": "https://discover.files.wordpress.com/2019/05/lonely-planet-kids-logo.png?w=150", + "medium": "https://discover.files.wordpress.com/2019/05/lonely-planet-kids-logo.png?w=315", + "large": "https://discover.files.wordpress.com/2019/05/lonely-planet-kids-logo.png?w=400" + }, + "height": 400, + "width": 400, + "exif": { + "aperture": "0", + "credit": "", + "camera": "", + "caption": "", + "created_timestamp": "0", + "copyright": "", + "focal_length": "0", + "iso": "0", + "shutter_speed": "0", + "title": "", + "orientation": "0", + "keywords": [] + }, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/media/37126", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/media/37126/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "parent": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37123" + } + } + } + }, + "attachment_count": 2, + "metadata": [ + { + "id": "129821", + "key": "geo_public", + "value": "0" + }, + { + "id": "129818", + "key": "_thumbnail_id", + "value": "37125" + }, + { + "id": "130369", + "key": "_wpas_done_17926349", + "value": "1" + }, + { + "id": "129822", + "key": "_wpas_mess", + "value": "Lonely Planet Kids (@lpkids) inspires children to be curious about the world. The #PoweredByWordPress site features children's books, activities, family travel resources, and more." + } + ], + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.1/read/sites/53424024/posts/37123", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/posts/37123/help", + "site": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "replies": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37123/replies/", + "likes": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/posts/37123/likes/" + }, + "data": { + "site": { + "ID": 53424024, + "name": "Discover", + "description": "A daily selection of the best content published on WordPress, collected for you by humans who love to read.", + "URL": "https://discover.wordpress.com", + "jetpack": false, + "post_count": 3603, + "subscribers_count": 35192480, + "locale": "en", + "icon": { + "img": "https://secure.gravatar.com/blavatar/c9e4e04719c81ca4936a63ea2dce6ace", + "ico": "https://secure.gravatar.com/blavatar/c9e4e04719c81ca4936a63ea2dce6ace" + }, + "logo": { + "id": 0, + "sizes": [], + "url": "" + }, + "visible": true, + "is_private": false, + "is_following": true, + "meta": { + "links": { + "self": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024", + "help": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/help", + "posts": "{{request.requestLine.baseUrl}}/rest/v1.2/sites/53424024/posts/", + "comments": "{{request.requestLine.baseUrl}}/rest/v1.1/sites/53424024/comments/", + "xmlrpc": "https://discover.wordpress.com/xmlrpc.php" + } + } + } + } + }, + "capabilities": { + "publish_post": false, + "delete_post": false, + "edit_post": false + }, + "other_URLs": {}, + "discover_metadata": { + "permalink": "https://www.lonelyplanet.com/kids/", + "attribution": { + "author_name": "Contributors", + "author_url": "https://www.lonelyplanet.com/kids/about/", + "blog_name": "Lonely Planet Kids", + "blog_url": "https://www.lonelyplanet.com", + "avatar_url": "https://discover.files.wordpress.com/2019/05/lonely-planet-kids-logo.png?w=100&h=100&crop=true" + }, + "discover_fp_post_formats": [ + { + "name": "Pick", + "slug": "pick", + "id": 346750 + }, + { + "name": "Site Pick", + "slug": "site-pick", + "id": 308219249 + } + ] + }, + "feed_ID": 41325786, + "feed_URL": "http://discover.wordpress.com", + "pseudo_ID": "12ac818a3de41e3b0cf84fea7efa2592", + "is_external": false, + "site_name": "Discover", + "site_URL": "https://discover.wordpress.com", + "site_is_private": false, + "featured_media": { + "uri": "https://discover.files.wordpress.com/2019/05/screen-shot-2019-05-10-at-3.53.09-pm.png", + "width": 1434, + "height": 808, + "type": "image" + }, + "use_excerpt": false, + "is_following_conversation": false + } + }, { + "type": "recommended_blogs", + "data": [{ + "description": "A South Staffordshire Wildlife Journal", + "feed_ID": 49407045, + "feed_URL": "http:\/\/petehillmansnaturephotography.wordpress.com", + "icon": { + "img": "https:\/\/petehillmansnaturephotography.files.wordpress.com\/2020\/09\/cropped-peter-hillman-a-nature-journey.jpg?w=96", + "ico": "https:\/\/petehillmansnaturephotography.files.wordpress.com\/2020\/09\/cropped-peter-hillman-a-nature-journey.jpg?w=96" + }, + "ID": 112482965, + "is_private": false, + "jetpack": false, + "name": "A Nature Journey", + "prefer_feed": false, + "subscribers_count": 1968, + "subscription": { + "delivery_methods": { + "email": null, + "notification": { + "send_posts": false + } + } + }, + "URL": "http:\/\/petehillmansnaturephotography.wordpress.com" + }, { + "description": "Jane's Lens", + "feed_ID": 1366484, + "feed_URL": "http:\/\/janeluriephotography.wordpress.com", + "icon": { + "img": "https:\/\/secure.gravatar.com\/blavatar\/3d272d8f6c1070fe12a4b778f2058c72", + "ico": "https:\/\/secure.gravatar.com\/blavatar\/3d272d8f6c1070fe12a4b778f2058c72" + }, + "ID": 26839598, + "is_private": false, + "jetpack": false, + "name": "Jane Lurie Photography", + "prefer_feed": false, + "subscribers_count": 6920, + "subscription": { + "delivery_methods": { + "email": null, + "notification": { + "send_posts": false + } + } + }, + "URL": "http:\/\/janeluriephotography.wordpress.com" + }] + }, { + "type": "post", + "data": { + "ID": 10046, + "site_ID": 28958452, + "author": { + "ID": 28500267, + "login": "terriwebsterschrandt", + "email": false, + "name": "Terri Webster Schrandt", + "first_name": "Terri", + "last_name": "Webster Schrandt", + "nice_name": "terriwebsterschrandt", + "URL": "http:\/\/terriwebsterschrandt.wordpress.com", + "avatar_URL": "https:\/\/2.gravatar.com\/avatar\/8870e170782893e9891d83bc57e9c8df?s=96&d=retro&r=G", + "profile_URL": "https:\/\/en.gravatar.com\/terriwebsterschrandt", + "site_ID": 28958452, + "has_avatar": true + }, + "date": "2020-09-27T07:00:00-07:00", + "modified": "2020-09-26T16:05:26-07:00", + "title": "Sunday Stills: Wishing for Water, But #Droplets Will Do", + "URL": "https:\/\/secondwindleisure.com\/2020\/09\/27\/sunday-stills-wishing-for-water-but-droplets-will-do\/", + "short_URL": "https:\/\/wp.me\/p1XvpO-2C2", + "content": "\n

My return to blogging was fun and satisfying after a tumultuous break! Thank you for welcoming me back last week. It was great to catch up with you!<\/p>\n\n\n\n

If you are confused by this week\u2019s Sunday Stills post, we are examining the world of water droplets. In my dry, still excessively warm part of the world, if I want water, I must turn on the garden hose. Artificially produced water droplets will work! <\/p>\n\n\n\n

\"End<\/figure>\n\n\n\n

After the few weeks of turmoil I recently experienced, I find calm and peace in my backyard garden any time of year. <\/p>\n\n\n\n

During our recent visit to Spokane, a few raindrops made their presence known by the end of the week.<\/p>\n\n\n\n

\"Evergreen<\/figure>\n\n\n\n

After a long, dry spell, my current library of water droplets is depleted, so please enjoy a few of my favorites from the past:<\/p>\n\n\n\n

\"House<\/figure>\n\n\n\n

My plumeria blossomed last year but no blooms this year. I\u2019m pretty sure I blew up social media and my blog with images of plumeria last summer. Here are a couple donning their droplets from daily backyard watering.<\/p>\n\n\n\n