From 13727b3f62423e1569f4ed4f12f1ffc5038eef6a Mon Sep 17 00:00:00 2001 From: develric Date: Mon, 16 Nov 2020 16:37:39 +0100 Subject: [PATCH 01/81] Setting up a feature flag for follow unfollow comments. --- WordPress/build.gradle | 2 ++ .../config/FollowUnfollowCommentsFeatureConfig.kt | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/util/config/FollowUnfollowCommentsFeatureConfig.kt diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 279997551a3a..8aa3e03e6942 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -79,6 +79,7 @@ android { buildConfigField "boolean", "ACTIVITY_LOG_FILTERS", "false" buildConfigField "boolean", "SCAN_AVAILABLE", "false" buildConfigField "boolean", "ENABLE_FEATURE_CONFIGURATION", "true" + buildConfigField "boolean", "FOLLOW_UNFOLLOW_COMMENTS", "false" } // Gutenberg's dependency - react-native-video is using @@ -121,6 +122,7 @@ android { buildConfigField "boolean", "HOME_PAGE_PICKER", "true" // Enable this for testing consolidated media picker // buildConfigField "boolean", "CONSOLIDATED_MEDIA_PICKER", "true" + buildConfigField "boolean", "FOLLOW_UNFOLLOW_COMMENTS", "true" } jalapeno { // Pre-Alpha version, used for PR builds, can be installed along release, alpha, beta, dev versions diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/FollowUnfollowCommentsFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/FollowUnfollowCommentsFeatureConfig.kt new file mode 100644 index 000000000000..fe2c01aa4c05 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/FollowUnfollowCommentsFeatureConfig.kt @@ -0,0 +1,15 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.FeatureInDevelopment +import javax.inject.Inject + +/** + * Configuration of the Follow Unfollow Comments + */ +@FeatureInDevelopment +class FollowUnfollowCommentsFeatureConfig +@Inject constructor(appConfig: AppConfig) : FeatureConfig( + appConfig, + BuildConfig.FOLLOW_UNFOLLOW_COMMENTS +) From 213080b0ded7a6d2d97ef2ab2631c7102adfd6ce Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 22 Nov 2020 11:11:41 -0500 Subject: [PATCH 02/81] Add status service for backup download --- .../BackupDownloadProgressChecker.kt | 4 +- .../BackupDownloadStatusService.kt | 212 ++++++++++++ .../BackupDownloadStatusServiceTest.kt | 301 ++++++++++++++++++ 3 files changed, 515 insertions(+), 2 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/activitylog/BackupDownloadStatusService.kt create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/activitylog/BackupDownloadStatusServiceTest.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/BackupDownloadProgressChecker.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/BackupDownloadProgressChecker.kt index 17515c63c70b..bd9c649b0201 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/BackupDownloadProgressChecker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/BackupDownloadProgressChecker.kt @@ -21,8 +21,8 @@ class BackupDownloadProgressChecker @Inject constructor( private val activityLogStore: ActivityLogStore, @param:Named(DEFAULT_SCOPE) private val defaultScope: CoroutineScope ) { - suspend fun startNow(site: SiteModel, restoreId: Long): OnBackupDownloadStatusFetched? { - return start(site, restoreId, true) + suspend fun startNow(site: SiteModel, downloadId: Long): OnBackupDownloadStatusFetched? { + return start(site, downloadId, true) } suspend fun start( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/BackupDownloadStatusService.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/BackupDownloadStatusService.kt new file mode 100644 index 000000000000..238b6ded7751 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/BackupDownloadStatusService.kt @@ -0,0 +1,212 @@ +package org.wordpress.android.ui.activitylog + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.activity.ActivityLogModel +import org.wordpress.android.fluxc.model.activity.BackupDownloadStatusModel +import org.wordpress.android.fluxc.store.ActivityLogStore +import org.wordpress.android.fluxc.store.ActivityLogStore.BackupDownloadError +import org.wordpress.android.fluxc.store.ActivityLogStore.BackupDownloadPayload +import org.wordpress.android.fluxc.store.ActivityLogStore.BackupDownloadRequestTypes +import org.wordpress.android.fluxc.store.ActivityLogStore.BackupDownloadStatusError +import org.wordpress.android.fluxc.store.ActivityLogStore.FetchBackupDownloadStatePayload +import org.wordpress.android.fluxc.store.ActivityLogStore.OnBackupDownload +import org.wordpress.android.modules.UI_SCOPE +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +// todo: annmarie - add tracking key - may be rewindId const val REWIND_ID_TRACKING_KEY = "rewind_id" + +@Singleton +class BackupDownloadStatusService @Inject constructor( + private val activityLogStore: ActivityLogStore, + private val backupDownloadProgressChecker: BackupDownloadProgressChecker, + @param:Named(UI_SCOPE) private val uiScope: CoroutineScope +) { + private val mutableBackupDownloadAvailable = MutableLiveData() + private val mutableBackupDownloadError = MutableLiveData() + private val mutableBackupDownloadStatusFetchError = MutableLiveData() + private val mutableBackupDownloadProgress = MutableLiveData() + private var site: SiteModel? = null + private var activityLogModelItem: ActivityLogModel? = null + private var backupDownloadProgressCheckerJob: Job? = null + private var fetchBackupDownloadJob: Job? = null + + val preparingBackupDownloadActivityLogModel: ActivityLogModel? + get() = activityLogModelItem + + val backupDownloadAvailable: LiveData = mutableBackupDownloadAvailable + val backupDownloadError: LiveData = mutableBackupDownloadError + val backupDownloadStatusFetchError: LiveData = mutableBackupDownloadStatusFetchError + val backupDownloadProgress: LiveData = mutableBackupDownloadProgress + + val isBackupDownloadInProgress: Boolean + get() = backupDownloadProgress.value?.progress != null + + val isBackupDownloadAvailable: Boolean + get() = backupDownloadAvailable.value == true + + fun backupDownload(rewindId: String, site: SiteModel, types: BackupDownloadRequestTypes) = + uiScope.launch { + // todo: annmarie - implement tracking here once naming has been decided + // AnalyticsUtils.trackWithSiteDetails( + // AnalyticsTracker.Stat.ACTIVITY_LOG_PREPARE_BACKUP_DOWNLOAD_STARTED, + // site, mutableMapOf(REWIND_ID_TRACKING_KEY to rewindId as Any)) + + updateBackupDownloadProgress(rewindId, 0) + mutableBackupDownloadAvailable.value = false + mutableBackupDownloadError.value = null + + val backupDownloadResult = activityLogStore.backupDownload( + BackupDownloadPayload( + site, + rewindId, + types + ) + ) + onBackupDownload(backupDownloadResult) + } + + fun start(site: SiteModel) { + if (this.site == null) { + this.site = site + requestStatusUpdate() + reloadBackupDownloadStatus() + } + } + + fun stop() { + backupDownloadProgressCheckerJob?.cancel() + fetchBackupDownloadJob?.cancel() + if (site != null) { + site = null + } + } + + fun requestStatusUpdate() { + site?.let { + fetchBackupDownloadJob?.cancel() + fetchBackupDownloadJob = uiScope.launch { + val backupDownloadStatus = activityLogStore.fetchBackupDownloadState( + FetchBackupDownloadStatePayload(it) + ) + onBackupDownloadStatusFetched( + backupDownloadStatus.error, + backupDownloadStatus.isError + ) + } + } + } + + private fun reloadBackupDownloadStatus() { + site?.let { + val state = activityLogStore.getBackupDownloadStatusForSite(it) + state?.let { + updateBackupDownloadStatus(state) + } + } + } + + private fun updateBackupDownloadStatus(backupDownloadStatus: BackupDownloadStatusModel?) { + mutableBackupDownloadAvailable.value = backupDownloadStatus?.progress == null + + if (backupDownloadStatus != null) { + val downloadId = backupDownloadStatus.downloadId + if (backupDownloadProgressCheckerJob?.isActive != true) { + site?.let { + backupDownloadProgressCheckerJob = uiScope.launch { + val backupDownloadStatusFetched = backupDownloadProgressChecker.startNow( + it, + downloadId + ) + onBackupDownloadStatusFetched( + backupDownloadStatusFetched?.error, + backupDownloadStatusFetched?.isError == true + ) + } + } + } + updateBackupDownloadProgress( + backupDownloadStatus.rewindId, + backupDownloadStatus.progress + ) + if (backupDownloadStatus.progress == null) { + backupDownloadProgressCheckerJob?.cancel() + } + } else { + mutableBackupDownloadProgress.setValue(null) + } + } + + private fun onBackupDownloadStatusFetched( + backupDownloadStatusError: BackupDownloadStatusError?, + isError: Boolean + ) { + mutableBackupDownloadStatusFetchError.value = backupDownloadStatusError + if (isError) { + backupDownloadProgressCheckerJob?.cancel() + } + reloadBackupDownloadStatus() + } + + private fun onBackupDownload(event: OnBackupDownload) { + mutableBackupDownloadError.value = event.error + if (event.isError) { + mutableBackupDownloadAvailable.value = true + reloadBackupDownloadStatus() + updateBackupDownloadProgress( + event.rewindId, + 0, + event.error?.type?.toString() + ) + return + } + site?.let { + event.downloadId?.let { downloadId -> + backupDownloadProgressCheckerJob = uiScope.launch { + val backupDownloadStatusFetched = backupDownloadProgressChecker.start( + it, + downloadId + ) + onBackupDownloadStatusFetched( + backupDownloadStatusFetched?.error, + backupDownloadStatusFetched?.isError == true + ) + } + } + } + } + + private fun updateBackupDownloadProgress( + rewindId: String?, + progress: Int?, + backupDownloadError: String? = null + ) { + var activityItem = if (rewindId != null) activityLogStore.getActivityLogItemByRewindId( + rewindId + ) else null + if (activityItem == null && activityLogModelItem != null && activityLogModelItem?.rewindID == rewindId) { + activityItem = activityLogModelItem + } + if (activityItem != null) { + activityLogModelItem = activityItem + } + val backupDownloadProgress = BackupDownloadProgress( + activityItem, + progress, + backupDownloadError + ) + mutableBackupDownloadProgress.value = backupDownloadProgress + } + + data class BackupDownloadProgress( + val activityLogItem: ActivityLogModel?, + val progress: Int?, + val failureReason: String? = null + ) +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/activitylog/BackupDownloadStatusServiceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/activitylog/BackupDownloadStatusServiceTest.kt new file mode 100644 index 000000000000..9cd3ed4bbcc8 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/activitylog/BackupDownloadStatusServiceTest.kt @@ -0,0 +1,301 @@ +package org.wordpress.android.ui.activitylog + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.argumentCaptor +import com.nhaarman.mockitokotlin2.reset +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.wordpress.android.TEST_SCOPE +import org.wordpress.android.fluxc.action.ActivityLogAction.BACKUP_DOWNLOAD +import org.wordpress.android.fluxc.action.ActivityLogAction.FETCH_BACKUP_DOWNLOAD_STATE +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.activity.ActivityLogModel +import org.wordpress.android.fluxc.model.activity.BackupDownloadStatusModel +import org.wordpress.android.fluxc.store.ActivityLogStore +import org.wordpress.android.fluxc.store.ActivityLogStore.BackupDownloadError +import org.wordpress.android.fluxc.store.ActivityLogStore.BackupDownloadErrorType +import org.wordpress.android.fluxc.store.ActivityLogStore.BackupDownloadPayload +import org.wordpress.android.fluxc.store.ActivityLogStore.BackupDownloadRequestTypes +import org.wordpress.android.fluxc.store.ActivityLogStore.BackupDownloadStatusError +import org.wordpress.android.fluxc.store.ActivityLogStore.BackupDownloadStatusErrorType +import org.wordpress.android.fluxc.store.ActivityLogStore.FetchBackupDownloadStatePayload +import org.wordpress.android.fluxc.store.ActivityLogStore.OnBackupDownload +import org.wordpress.android.fluxc.store.ActivityLogStore.OnBackupDownloadStatusFetched +import org.wordpress.android.fluxc.tools.FormattableContent +import org.wordpress.android.ui.activitylog.BackupDownloadStatusService.BackupDownloadProgress +import java.util.Date + +@RunWith(MockitoJUnitRunner::class) +class BackupDownloadStatusServiceTest { + @Rule @JvmField val rule = InstantTaskExecutorRule() + + private val backupDownloadStatusCaptor = argumentCaptor() + private val backupDownloadCaptor = argumentCaptor() + + @Mock private lateinit var activityLogStore: ActivityLogStore + @Mock private lateinit var backupDownloadProgressChecker: BackupDownloadProgressChecker + @Mock private lateinit var site: SiteModel + + private lateinit var backupDownloadStatusService: BackupDownloadStatusService + private var backupDownloadAvailable: Boolean? = null + private var backupDownloadProgress: BackupDownloadProgress? = null + private var backupDownloadError: BackupDownloadError? = null + private var backupDownloadStatusFetchError: BackupDownloadStatusError? = null + + private val rewindId = "10" + private val downloadId = 10L + private val activityID = "activityId" + private val progress = 35 + private val backupPoint = Date() + private val startedAt = Date() + private val published = Date() + private val activityLogModel = ActivityLogModel( + activityID, + "summary", + FormattableContent(text = "text"), + null, + null, + null, + null, + null, + rewindId, + published + ) + + private val inProgressBackupDownloadStatusModel = BackupDownloadStatusModel( + downloadId = downloadId, + rewindId = rewindId, + backupPoint = backupPoint, + startedAt = startedAt, + progress = progress, + downloadCount = null, + validUntil = null, + url = null + ) + + private val types = BackupDownloadRequestTypes( + themes = true, + plugins = true, + uploads = true, + sqls = true, + roots = true, + contents = true + ) + + @Before + fun setUp() = runBlocking { + backupDownloadStatusService = BackupDownloadStatusService( + activityLogStore, + backupDownloadProgressChecker, + TEST_SCOPE + ) + backupDownloadAvailable = null + backupDownloadStatusService.backupDownloadAvailable.observeForever { backupDownloadAvailable = it } + backupDownloadStatusService.backupDownloadProgress.observeForever { backupDownloadProgress = it } + backupDownloadStatusService.backupDownloadError.observeForever { backupDownloadError = it } + backupDownloadStatusService.backupDownloadStatusFetchError.observeForever { + backupDownloadStatusFetchError = it } + whenever(activityLogStore.getBackupDownloadStatusForSite(site)).thenReturn(null) + whenever(activityLogStore.fetchBackupDownloadState(any())).thenReturn( + OnBackupDownloadStatusFetched(FETCH_BACKUP_DOWNLOAD_STATE) + ) + whenever(activityLogStore.backupDownload(any())).thenReturn( + OnBackupDownload( + rewindId = rewindId, + causeOfChange = BACKUP_DOWNLOAD + ) + ) + whenever(activityLogStore.getActivityLogItemByRewindId(rewindId)).thenReturn( + activityLogModel + ) + whenever(backupDownloadProgressChecker.startNow(any(), any())).thenReturn(null) + } + + @After + fun tearDown() { + backupDownloadStatusService.stop() + } + + @Test + fun `emits available BackupDownloadStatus on start when download not in progress`() { + whenever(activityLogStore.getBackupDownloadStatusForSite(site)).thenReturn( + inProgressBackupDownloadStatusModel.copy(progress = null) + ) + + backupDownloadStatusService.start(site) + + assertEquals(backupDownloadAvailable, true) + } + + @Test + fun `emits unavailable BackupDownloadStatus on start when download is in progress`() = runBlocking { + val inactiveBackupDownloadStatusModel = inProgressBackupDownloadStatusModel + whenever(activityLogStore.getBackupDownloadStatusForSite(site)).thenReturn( + inactiveBackupDownloadStatusModel, + null + ) + + backupDownloadStatusService.start(site) + + assertEquals(backupDownloadAvailable, false) + } + + @Test + fun `triggers fetch when BackupDownloadStatus not available`() = runBlocking { + backupDownloadStatusService.start(site) + + assertFetchBackupDownloadStatusAction() + } + + @Test + fun `updates BackupDownloadStatus and restarts checker when BackupDownload not already running`() = + runBlocking { + backupDownloadStatusService.start(site) + whenever(activityLogStore.getBackupDownloadStatusForSite(site)).thenReturn( + inProgressBackupDownloadStatusModel + ) + whenever(activityLogStore.fetchBackupDownloadState(any())).thenReturn( + OnBackupDownloadStatusFetched(FETCH_BACKUP_DOWNLOAD_STATE) + ) + reset(backupDownloadProgressChecker) + + backupDownloadStatusService.requestStatusUpdate() + + verify(backupDownloadProgressChecker).startNow( + site, + inProgressBackupDownloadStatusModel.downloadId + ) + } + + @Test + fun `triggers BackupDownload And Makes Action Unavailable`() = runBlocking { + val rewindId = "10" + + backupDownloadStatusService.backupDownload(rewindId, site, types) + + assertBackupDownloadAction(rewindId) + assertEquals(false, backupDownloadAvailable) + assertEquals(backupDownloadProgress, BackupDownloadProgress(activityLogModel, 0)) + } + + @Test + fun `cancels worker OnFetchErrorBackupDownloadState and emits error`() = runBlocking { + backupDownloadStatusService.start(site) + val error = BackupDownloadStatusError(BackupDownloadStatusErrorType.INVALID_RESPONSE, null) + whenever(activityLogStore.fetchBackupDownloadState(any())).thenReturn( + OnBackupDownloadStatusFetched(error, BACKUP_DOWNLOAD) + ) + + backupDownloadStatusService.requestStatusUpdate() + + assertEquals(error, backupDownloadStatusFetchError) + } + + @Test + fun `when onBackupDownloadState in progress update state`() = runBlocking { + backupDownloadStatusService.start(site) + + whenever(activityLogStore.getBackupDownloadStatusForSite(site)).thenReturn( + inProgressBackupDownloadStatusModel + ) + whenever(activityLogStore.fetchBackupDownloadState(any())).thenReturn( + OnBackupDownloadStatusFetched(FETCH_BACKUP_DOWNLOAD_STATE) + ) + + backupDownloadStatusService.requestStatusUpdate() + + assertEquals(backupDownloadAvailable, false) + assertEquals(backupDownloadProgress, BackupDownloadProgress(activityLogModel, progress)) + } + + @Test + fun `when onBackupDownloadState finished update state`() = runBlocking { + backupDownloadStatusService.start(site) + + val backupDownloadFinished = inProgressBackupDownloadStatusModel.copy(progress = null) + + whenever(activityLogStore.getBackupDownloadStatusForSite(site)).thenReturn( + inProgressBackupDownloadStatusModel, + backupDownloadFinished + ) + + whenever(activityLogStore.fetchBackupDownloadState(any())).thenReturn( + OnBackupDownloadStatusFetched(FETCH_BACKUP_DOWNLOAD_STATE) + ) + + backupDownloadStatusService.requestStatusUpdate() + + assertEquals(backupDownloadAvailable, true) + assertEquals(backupDownloadProgress?.progress, null) + } + + @Test + fun `when onBackupDownload error cancel worker and re-enable BackupDownloadStatus`() = runBlocking { + backupDownloadStatusService.start(site) + + whenever(activityLogStore.getBackupDownloadStatusForSite(site)).thenReturn( + inProgressBackupDownloadStatusModel + ) + + val error = BackupDownloadError(BackupDownloadErrorType.INVALID_RESPONSE, null) + whenever(activityLogStore.backupDownload(any())).thenReturn( + OnBackupDownload( + rewindId, + error, + BACKUP_DOWNLOAD + ) + ) + + backupDownloadAvailable = null + + backupDownloadStatusService.backupDownload(rewindId, site, types) + + assertEquals(backupDownloadAvailable, false) + assertEquals(error, backupDownloadError) + val progress = BackupDownloadProgress(activityLogModel, progress) + assertEquals(backupDownloadProgress, progress) + } + + @Test + fun `onBackupDownloadFetchStatus start worker`() = runBlocking { + backupDownloadStatusService.start(site) + reset(backupDownloadProgressChecker) + + whenever(activityLogStore.backupDownload(any())).thenReturn( + OnBackupDownload( + rewindId = rewindId, + downloadId = downloadId, + causeOfChange = BACKUP_DOWNLOAD + ) + ) + + backupDownloadStatusService.backupDownload(rewindId, site, types) + + verify(backupDownloadProgressChecker).start(site, downloadId) + } + + private suspend fun assertFetchBackupDownloadStatusAction() { + verify(activityLogStore).fetchBackupDownloadState(backupDownloadStatusCaptor.capture()) + backupDownloadStatusCaptor.firstValue.apply { + assertEquals(this.site, site) + } + } + + private suspend fun assertBackupDownloadAction(rewindId: String) { + verify(activityLogStore).backupDownload(backupDownloadCaptor.capture()) + backupDownloadCaptor.firstValue.apply { + assertEquals(this.site, site) + assertEquals(this.rewindId, rewindId) + } + } +} From 7c5122c2c9d2bbb9b7fc40a7d74503c53ed19ed7 Mon Sep 17 00:00:00 2001 From: Cameron Voell Date: Mon, 23 Nov 2020 10:47:46 -0800 Subject: [PATCH 03/81] Release script: Update gutenberg-mobile ref --- libs/gutenberg-mobile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/gutenberg-mobile b/libs/gutenberg-mobile index cac7655ef1c1..0a97fc5cf823 160000 --- a/libs/gutenberg-mobile +++ b/libs/gutenberg-mobile @@ -1 +1 @@ -Subproject commit cac7655ef1c1dcf2de8e73bab426816713e86e08 +Subproject commit 0a97fc5cf823f953e86f878e322b4fddd828635c From a1afbd78468bce066a18c94ce88b587212982c60 Mon Sep 17 00:00:00 2001 From: Cameron Voell Date: Mon, 23 Nov 2020 10:47:48 -0800 Subject: [PATCH 04/81] Release script: Update strings --- WordPress/src/main/res/values/strings.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index f396919c036a..fdcad217db5f 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -3061,6 +3061,7 @@ Block settings Blog + CHOOSE A FILE Choose from device Choose image Choose image or video @@ -3071,6 +3072,7 @@ Content… Copied block Copy block + Copy file URL Current value is %s CUSTOMIZE @@ -3105,12 +3107,17 @@ Dr. Seuss Duplicate block + Edit file Edit using web editor Edit video Email me: <a href=\"mailto:mail@example.com\">mail@example.com</a> Excerpt length (words) Failed to insert media.\nPlease tap for options. + Failed to save files.\nPlease tap for options. + Failed to upload files.\nPlease tap for options. + File block settings + File name Gallery caption. %s Hide keyboard Image caption. %s - Insert mention Inspiration From 2c18e53ccc67b227597903f95e597f01cd4c20da Mon Sep 17 00:00:00 2001 From: khaykov Date: Mon, 23 Nov 2020 15:20:53 -0800 Subject: [PATCH 05/81] Added trackers. --- .../android/analytics/AnalyticsTracker.java | 17 ++++++++++ .../analytics/AnalyticsTrackerNosara.java | 34 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index 86c8c8312054..d0d13db0dbdf 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -690,6 +690,23 @@ public enum Stat { ENCRYPTED_LOGGING_UPLOAD_SUCCESSFUL, ENCRYPTED_LOGGING_UPLOAD_FAILED, READER_POST_REPORTED, + COMMENT_APPROVED, + COMMENT_UNAPPROVED, + COMMENT_SPAMMED, + COMMENT_UNSPAMMED, + COMMENT_FOLLOWED, + COMMENT_UNFOLLOWED, + COMMENT_LIKED, + COMMENT_UNLIKED, + COMMENT_TRASHED, + COMMENT_UNTRASHED, + COMMENT_REPLIED_TO, + COMMENT_EDITED, + COMMENT_VIEWED, + COMMENT_DELETED, + COMMENT_QUICK_ACTION_APPROVED, + COMMENT_QUICK_ACTION_LIKED, + COMMENT_QUICK_ACTION_REPLIED_TO, } private static final List TRACKERS = new ArrayList<>(); diff --git a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java index 1c998a11aee7..e8ff2bea51b5 100644 --- a/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java +++ b/libs/analytics/WordPressAnalytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java @@ -229,6 +229,9 @@ public void track(AnalyticsTracker.Stat stat, Map properties) { case NOTIFICATION_QUICK_ACTIONS_LIKED: case NOTIFICATION_QUICK_ACTIONS_REPLIED_TO: case NOTIFICATION_QUICK_ACTIONS_APPROVED: + case COMMENT_QUICK_ACTION_LIKED: + case COMMENT_QUICK_ACTION_REPLIED_TO: + case COMMENT_QUICK_ACTION_APPROVED: predefinedEventProperties.put("is_quick_action", true); break; case SIGNUP_EMAIL_EPILOGUE_UNCHANGED: @@ -1891,6 +1894,37 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) { return "encrypted_logging_upload_failed"; case READER_POST_REPORTED: return "reader_post_reported"; + case COMMENT_APPROVED: + case COMMENT_QUICK_ACTION_APPROVED: + return "comment_approved"; + case COMMENT_UNAPPROVED: + return "comment_unapproved"; + case COMMENT_SPAMMED: + return "comment_spammed"; + case COMMENT_UNSPAMMED: + return "comment_unspammed"; + case COMMENT_FOLLOWED: + return "comment_followed"; + case COMMENT_UNFOLLOWED: + return "comment_unfollowed"; + case COMMENT_LIKED: + case COMMENT_QUICK_ACTION_LIKED: + return "comment_liked"; + case COMMENT_UNLIKED: + return "comment_unliked"; + case COMMENT_TRASHED: + return "comment_trashed"; + case COMMENT_UNTRASHED: + return "comment_untrashed"; + case COMMENT_REPLIED_TO: + case COMMENT_QUICK_ACTION_REPLIED_TO: + return "comment_replied_to"; + case COMMENT_EDITED: + return "comment_edited"; + case COMMENT_VIEWED: + return "comment_viewed"; + case COMMENT_DELETED: + return "comment_deleted"; } return null; } From 6da16fc1bc9830d90b972daa9dfbdcb76d584402 Mon Sep 17 00:00:00 2001 From: khaykov Date: Mon, 23 Nov 2020 15:21:27 -0800 Subject: [PATCH 06/81] Added comment related tracking methods to AnalyticsUtils --- .../util/analytics/AnalyticsUtils.java | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/util/analytics/AnalyticsUtils.java b/WordPress/src/main/java/org/wordpress/android/util/analytics/AnalyticsUtils.java index 4765e0f4e2dd..844dd480fe1a 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/analytics/AnalyticsUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/analytics/AnalyticsUtils.java @@ -75,6 +75,7 @@ public class AnalyticsUtils { private static final String INTERCEPTOR_CLASSNAME = "interceptor_classname"; private static final String NEWS_CARD_ORIGIN = "origin"; private static final String NEWS_CARD_VERSION = "version"; + private static final String COMMENT_ACTION_SOURCE = "source"; public static final String HAS_GUTENBERG_BLOCKS_KEY = "has_gutenberg_blocks"; public static final String HAS_WP_STORIES_BLOCKS_KEY = "has_wp_stories_blocks"; @@ -306,13 +307,24 @@ public static void trackQuickActionTouched(QuickActionTrackPropertyValue type, * @param comment The comment object */ public static void trackCommentReplyWithDetails(boolean isQuickReply, SiteModel site, - CommentModel comment) { - AnalyticsTracker.Stat stat = isQuickReply ? AnalyticsTracker.Stat.NOTIFICATION_QUICK_ACTIONS_REPLIED_TO - : AnalyticsTracker.Stat.NOTIFICATION_REPLIED_TO; + CommentModel comment, AnalyticsCommentActionSource source) { + AnalyticsTracker.Stat legacyTracker = null; + if (source == AnalyticsCommentActionSource.NOTIFICATIONS) { + legacyTracker = isQuickReply ? AnalyticsTracker.Stat.NOTIFICATION_QUICK_ACTIONS_REPLIED_TO + : AnalyticsTracker.Stat.NOTIFICATION_REPLIED_TO; + } + + AnalyticsTracker.Stat stat = isQuickReply ? Stat.COMMENT_QUICK_ACTION_REPLIED_TO + : Stat.COMMENT_REPLIED_TO; if (site == null || !SiteUtils.isAccessedViaWPComRest(site)) { AppLog.w(AppLog.T.STATS, "The passed blog obj is null or it's not a wpcom or Jetpack." + " Tracking analytics without blog info"); AnalyticsTracker.track(stat); + + if (legacyTracker != null) { + AnalyticsTracker.track(legacyTracker); + } + return; } @@ -321,8 +333,13 @@ public static void trackCommentReplyWithDetails(boolean isQuickReply, SiteModel properties.put(IS_JETPACK_KEY, site.isJetpackConnected()); properties.put(POST_ID_KEY, comment.getRemotePostId()); properties.put(COMMENT_ID_KEY, comment.getRemoteCommentId()); + properties.put(COMMENT_ACTION_SOURCE, source.toString()); AnalyticsTracker.track(stat, properties); + + if (legacyTracker != null) { + AnalyticsTracker.track(legacyTracker, properties); + } } @@ -347,13 +364,20 @@ public static void trackWithSiteId(AnalyticsTracker.Stat stat, long blogID) { * @param post The reader post to track */ public static void trackWithReaderPostDetails(AnalyticsTracker.Stat stat, ReaderPost post) { + trackWithReaderPostDetails(stat, post, null); + } + + public static void trackWithReaderPostDetails(AnalyticsTracker.Stat stat, ReaderPost post, + Map properties) { if (post == null) { return; } // wpcom/jetpack posts should pass: feed_id, feed_item_id, blog_id, post_id, is_jetpack // RSS pass should pass: feed_id, feed_item_id, is_jetpack - Map properties = new HashMap<>(); + if (properties == null) { + properties = new HashMap<>(); + } if (post.isWP() || post.isJetpack) { properties.put(BLOG_ID_KEY, post.blogId); properties.put(POST_ID_KEY, post.postId); @@ -622,4 +646,39 @@ public static void trackLoginProloguePages(int page) { properties.put("page_number", page); AnalyticsTracker.track(Stat.LOGIN_PROLOGUE_PAGED, properties); } + + public enum AnalyticsCommentActionSource { + NOTIFICATIONS { + public String toString() { + return "notifications"; + } + }, + SITE_COMMENTS { + public String toString() { + return "site_comments"; + } + }, + READER { + public String toString() { + return "reader"; + } + } + } + + public static void trackCommentActionWithSiteDetails(AnalyticsTracker.Stat stat, + AnalyticsCommentActionSource actionSource, SiteModel site) { + Map properties = new HashMap<>(); + properties.put(COMMENT_ACTION_SOURCE, actionSource.toString()); + + AnalyticsUtils.trackWithSiteDetails(stat, site, properties); + } + + + public static void trackCommentActionWithReaderPostDetails(AnalyticsTracker.Stat stat, + AnalyticsCommentActionSource actionSource, ReaderPost post) { + Map properties = new HashMap<>(); + properties.put(COMMENT_ACTION_SOURCE, actionSource.toString()); + + AnalyticsUtils.trackWithReaderPostDetails(stat, post, properties); + } } From 4418c9eb34489eff06ac5dbe13d34edb336a3196 Mon Sep 17 00:00:00 2001 From: khaykov Date: Mon, 23 Nov 2020 15:22:27 -0800 Subject: [PATCH 07/81] Tracking comment actions in details and notifications. --- .../push/NotificationsProcessingService.java | 20 +++-- .../ui/comments/CommentDetailFragment.java | 86 ++++++++++++++++--- 2 files changed, 88 insertions(+), 18 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/push/NotificationsProcessingService.java b/WordPress/src/main/java/org/wordpress/android/push/NotificationsProcessingService.java index 41ba7e0cb632..3c278b46da57 100644 --- a/WordPress/src/main/java/org/wordpress/android/push/NotificationsProcessingService.java +++ b/WordPress/src/main/java/org/wordpress/android/push/NotificationsProcessingService.java @@ -49,6 +49,7 @@ import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.LocaleManager; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource; import org.wordpress.android.util.analytics.AnalyticsUtils.QuickActionTrackPropertyValue; import java.util.ArrayList; @@ -543,15 +544,18 @@ private void likeComment() { return; } + SiteModel site = mSiteStore.getSiteBySiteId(mNote.getSiteId()); + // Bump analytics AnalyticsUtils.trackWithBlogPostDetails( AnalyticsTracker.Stat.NOTIFICATION_QUICK_ACTIONS_LIKED, mNote.getSiteId(), mNote.getPostId()); + AnalyticsUtils.trackCommentActionWithSiteDetails(Stat.COMMENT_QUICK_ACTION_LIKED, + AnalyticsCommentActionSource.NOTIFICATIONS, site); AnalyticsUtils.trackQuickActionTouched( QuickActionTrackPropertyValue.LIKE, - mSiteStore.getSiteBySiteId(mNote.getSiteId()), + site, mNote.buildComment()); - SiteModel site = mSiteStore.getSiteBySiteId(mNote.getSiteId()); if (site != null) { mDispatcher.dispatch(CommentActionBuilder.newLikeCommentAction( new RemoteLikeCommentPayload(site, mNote.getCommentId(), true))); @@ -567,18 +571,23 @@ private void approveComment() { return; } + SiteModel site = mSiteStore.getSiteBySiteId(mNote.getSiteId()); + // Bump analytics AnalyticsUtils.trackWithBlogPostDetails( AnalyticsTracker.Stat.NOTIFICATION_QUICK_ACTIONS_APPROVED, mNote.getSiteId(), mNote.getPostId()); + AnalyticsUtils.trackCommentActionWithSiteDetails(Stat.COMMENT_QUICK_ACTION_APPROVED, + AnalyticsCommentActionSource.NOTIFICATIONS, site); + AnalyticsUtils.trackQuickActionTouched( QuickActionTrackPropertyValue.APPROVE, - mSiteStore.getSiteBySiteId(mNote.getSiteId()), + site, mNote.buildComment()); // Update pseudo comment (built from the note) CommentModel comment = mNote.buildComment(); comment.setStatus(CommentStatus.APPROVED.toString()); - SiteModel site = mSiteStore.getSiteBySiteId(mNote.getSiteId()); + if (site == null) { AppLog.e(T.NOTIFS, "Impossible to approve a comment on a site that is not in the App. SiteId: " + mNote.getSiteId()); @@ -621,7 +630,8 @@ private void replyToComment() { mDispatcher.dispatch(CommentActionBuilder.newCreateNewCommentAction(payload)); // Bump analytics - AnalyticsUtils.trackCommentReplyWithDetails(true, site, comment); + AnalyticsUtils + .trackCommentReplyWithDetails(true, site, comment, AnalyticsCommentActionSource.NOTIFICATIONS); AnalyticsUtils.trackQuickActionTouched(QuickActionTrackPropertyValue.REPLY_TO, site, comment); } else { // cancel the current notification diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java index 3b1058330a77..8a0951eaab90 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java @@ -96,6 +96,7 @@ import org.wordpress.android.util.ViewUtilsKt; import org.wordpress.android.util.WPLinkMovementMethod; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.util.image.ImageType; import org.wordpress.android.widgets.SuggestionAutoCompleteText; @@ -119,8 +120,22 @@ public class CommentDetailFragment extends ViewPagerFragment implements Notifica private static final String KEY_REPLY_TEXT = "KEY_REPLY_TEXT"; private static final int INTENT_COMMENT_EDITOR = 1010; - private static final int FROM_BLOG_COMMENT = 1; - private static final int FROM_NOTE = 2; + + enum CommentSource { + NOTIFICATION, + SITE_COMMENTS; + + AnalyticsCommentActionSource toAnalyticsCommentActionSource() { + switch (this) { + case NOTIFICATION: + return AnalyticsCommentActionSource.NOTIFICATIONS; + case SITE_COMMENTS: + return AnalyticsCommentActionSource.SITE_COMMENTS; + } + throw new IllegalArgumentException( + this + " CommentSource is not mapped to corresponding AnalyticsCommentActionSource"); + } + } private CommentModel mComment; private SiteModel mSite; @@ -167,6 +182,8 @@ public class CommentDetailFragment extends ViewPagerFragment implements Notifica private OnCommentActionListener mOnCommentActionListener; private OnNoteCommentActionListener mOnNoteCommentActionListener; + private CommentSource mCommentSource; + /* * these determine which actions (moderation, replying, marking as spam) to enable * for this comment - all actions are enabled when opened from the comment list, only @@ -180,7 +197,7 @@ public class CommentDetailFragment extends ViewPagerFragment implements Notifica static CommentDetailFragment newInstance(SiteModel site, CommentModel commentModel) { CommentDetailFragment fragment = new CommentDetailFragment(); Bundle args = new Bundle(); - args.putInt(KEY_MODE, FROM_BLOG_COMMENT); + args.putSerializable(KEY_MODE, CommentSource.SITE_COMMENTS); args.putInt(KEY_SITE_LOCAL_ID, site.getId()); args.putLong(KEY_COMMENT_ID, commentModel.getRemoteCommentId()); fragment.setArguments(args); @@ -193,7 +210,7 @@ static CommentDetailFragment newInstance(SiteModel site, CommentModel commentMod public static CommentDetailFragment newInstance(final String noteId, final String replyText) { CommentDetailFragment fragment = new CommentDetailFragment(); Bundle args = new Bundle(); - args.putInt(KEY_MODE, FROM_NOTE); + args.putSerializable(KEY_MODE, CommentSource.NOTIFICATION); args.putString(KEY_NOTE_ID, noteId); args.putString(KEY_REPLY_TEXT, replyText); fragment.setArguments(args); @@ -205,11 +222,13 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((WordPress) getActivity().getApplication()).component().inject(this); - switch (getArguments().getInt(KEY_MODE)) { - case FROM_BLOG_COMMENT: + mCommentSource = (CommentSource) getArguments().getSerializable(KEY_MODE); + + switch (mCommentSource) { + case SITE_COMMENTS: setComment(getArguments().getLong(KEY_COMMENT_ID), getArguments().getInt(KEY_SITE_LOCAL_ID)); break; - case FROM_NOTE: + case NOTIFICATION: setNote(getArguments().getString(KEY_NOTE_ID)); setReplyText(getArguments().getString(KEY_REPLY_TEXT)); break; @@ -589,6 +608,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == INTENT_COMMENT_EDITOR && resultCode == Activity.RESULT_OK) { reloadComment(); + AnalyticsUtils.trackCommentActionWithSiteDetails(Stat.COMMENT_EDITED, + mCommentSource.toAnalyticsCommentActionSource(), mSite); } } @@ -721,6 +742,9 @@ private void showComment() { } } + AnalyticsUtils.trackCommentActionWithSiteDetails( + Stat.COMMENT_VIEWED, mCommentSource.toAnalyticsCommentActionSource(), mSite); + getActivity().invalidateOptionsMenu(); } @@ -841,16 +865,46 @@ public void onFailure(int statusCode) { private void trackModerationFromNotification(final CommentStatus newStatus) { switch (newStatus) { case APPROVED: - AnalyticsTracker.track(Stat.NOTIFICATION_APPROVED); + if (mCommentSource == CommentSource.NOTIFICATION) { + AnalyticsTracker.track(Stat.NOTIFICATION_APPROVED); + } + AnalyticsUtils.trackCommentActionWithSiteDetails(Stat.COMMENT_APPROVED, + mCommentSource.toAnalyticsCommentActionSource(), mSite); break; case UNAPPROVED: - AnalyticsTracker.track(Stat.NOTIFICATION_UNAPPROVED); + if (mCommentSource == CommentSource.NOTIFICATION) { + AnalyticsTracker.track(Stat.NOTIFICATION_UNAPPROVED); + } + AnalyticsUtils.trackCommentActionWithSiteDetails(Stat.COMMENT_UNAPPROVED, + mCommentSource.toAnalyticsCommentActionSource(), mSite); break; case SPAM: - AnalyticsTracker.track(Stat.NOTIFICATION_FLAGGED_AS_SPAM); + if (mCommentSource == CommentSource.NOTIFICATION) { + AnalyticsTracker.track(Stat.NOTIFICATION_FLAGGED_AS_SPAM); + } + AnalyticsUtils.trackCommentActionWithSiteDetails(Stat.COMMENT_SPAMMED, + mCommentSource.toAnalyticsCommentActionSource(), mSite); + break; + case UNSPAM: + AnalyticsUtils.trackCommentActionWithSiteDetails(Stat.COMMENT_UNSPAMMED, + mCommentSource.toAnalyticsCommentActionSource(), mSite); break; case TRASH: - AnalyticsTracker.track(Stat.NOTIFICATION_TRASHED); + if (mCommentSource == CommentSource.NOTIFICATION) { + AnalyticsTracker.track(Stat.NOTIFICATION_TRASHED); + } + AnalyticsUtils.trackCommentActionWithSiteDetails(Stat.COMMENT_TRASHED, + mCommentSource.toAnalyticsCommentActionSource(), mSite); + break; + case UNTRASH: + AnalyticsUtils.trackCommentActionWithSiteDetails(Stat.COMMENT_UNTRASHED, + mCommentSource.toAnalyticsCommentActionSource(), mSite); + break; + case DELETED: + AnalyticsUtils.trackCommentActionWithSiteDetails(Stat.COMMENT_DELETED, + mCommentSource.toAnalyticsCommentActionSource(), mSite); + break; + case ALL: break; } } @@ -920,7 +974,8 @@ private void submitReply() { mIsSubmittingReply = true; - AnalyticsUtils.trackCommentReplyWithDetails(false, mSite, mComment); + AnalyticsUtils.trackCommentReplyWithDetails( + false, mSite, mComment, mCommentSource.toAnalyticsCommentActionSource()); // Pseudo comment reply CommentModel reply = new CommentModel(); @@ -1167,7 +1222,12 @@ private void likeComment(boolean forceLike) { ReaderAnim.animateLikeButton(mBtnLikeIcon, mBtnLikeComment.isActivated()); // Bump analytics - AnalyticsTracker.track(mBtnLikeComment.isActivated() ? Stat.NOTIFICATION_LIKED : Stat.NOTIFICATION_UNLIKED); + if (mCommentSource == CommentSource.NOTIFICATION) { + AnalyticsTracker.track(mBtnLikeComment.isActivated() ? Stat.NOTIFICATION_LIKED : Stat.NOTIFICATION_UNLIKED); + } + AnalyticsUtils.trackCommentActionWithSiteDetails( + mBtnLikeComment.isActivated() ? Stat.COMMENT_LIKED : Stat.COMMENT_UNLIKED, + mCommentSource.toAnalyticsCommentActionSource(), mSite); if (mNotificationsDetailListFragment != null && mComment != null) { // Optimistically set comment to approved when liking an unapproved comment From 8739ba4c45b16d0ae30af144828aca072a7d6f61 Mon Sep 17 00:00:00 2001 From: khaykov Date: Mon, 23 Nov 2020 15:22:44 -0800 Subject: [PATCH 08/81] Tracking comment actions in reader. --- .../android/ui/reader/ReaderCommentListActivity.java | 6 ++++++ .../android/ui/reader/adapters/ReaderCommentAdapter.java | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java index 0e52588cb978..f00e68e20d18 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java @@ -38,6 +38,7 @@ import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.datasets.ReaderCommentTable; import org.wordpress.android.datasets.ReaderPostTable; import org.wordpress.android.datasets.SuggestionTable; @@ -76,6 +77,7 @@ import org.wordpress.android.util.ViewUtilsKt; import org.wordpress.android.util.WPActivityUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource; import org.wordpress.android.util.helpers.SwipeToRefreshHelper; import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout; import org.wordpress.android.widgets.RecyclerItemDecoration; @@ -260,6 +262,8 @@ public void afterTextChanged(Editable s) { } AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_COMMENTS_OPENED, mPost); + AnalyticsUtils.trackCommentActionWithReaderPostDetails(AnalyticsTracker.Stat.COMMENT_VIEWED, + AnalyticsCommentActionSource.READER, mPost); mSite = mSiteStore.getSiteBySiteId(mBlogId); @@ -604,6 +608,8 @@ && getCommentAdapter().refreshComment(mCommentId)) { AnalyticsUtils.trackWithReaderPostDetails( AnalyticsTracker.Stat.READER_ARTICLE_COMMENT_LIKED, mPost); + AnalyticsUtils.trackCommentActionWithReaderPostDetails(Stat.COMMENT_LIKED, + AnalyticsCommentActionSource.READER, mPost); } else { ToastUtils.showToast(ReaderCommentListActivity.this, R.string.reader_toast_err_generic); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java index 215f77ca6076..b67fe5948c3b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java @@ -16,6 +16,7 @@ import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.datasets.ReaderCommentTable; import org.wordpress.android.datasets.ReaderPostTable; import org.wordpress.android.fluxc.store.AccountStore; @@ -43,6 +44,7 @@ import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.util.image.ImageType; @@ -414,6 +416,9 @@ private void toggleLike(Context context, CommentHolder holder, int position) { AnalyticsUtils.trackWithReaderPostDetails(isAskingToLike ? AnalyticsTracker.Stat.READER_ARTICLE_COMMENT_LIKED : AnalyticsTracker.Stat.READER_ARTICLE_COMMENT_UNLIKED, mPost); + AnalyticsUtils.trackCommentActionWithReaderPostDetails( + isAskingToLike ? Stat.COMMENT_LIKED : Stat.COMMENT_UNLIKED, + AnalyticsCommentActionSource.READER, mPost); } public boolean refreshComment(long commentId) { From 8767e2ebcccae9a9fa9e52d6b2e4abffec23af0b Mon Sep 17 00:00:00 2001 From: develric Date: Tue, 24 Nov 2020 18:40:58 +0100 Subject: [PATCH 09/81] Adding icon resources. --- .../ic_reader_follow_conversation_white_24dp.xml | 12 ++++++++++++ .../ic_reader_following_conversation_white_24dp.xml | 12 ++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 WordPress/src/main/res/drawable/ic_reader_follow_conversation_white_24dp.xml create mode 100644 WordPress/src/main/res/drawable/ic_reader_following_conversation_white_24dp.xml diff --git a/WordPress/src/main/res/drawable/ic_reader_follow_conversation_white_24dp.xml b/WordPress/src/main/res/drawable/ic_reader_follow_conversation_white_24dp.xml new file mode 100644 index 000000000000..1cda78fc35e4 --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_reader_follow_conversation_white_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/WordPress/src/main/res/drawable/ic_reader_following_conversation_white_24dp.xml b/WordPress/src/main/res/drawable/ic_reader_following_conversation_white_24dp.xml new file mode 100644 index 000000000000..e72fceb38ec4 --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_reader_following_conversation_white_24dp.xml @@ -0,0 +1,12 @@ + + + + From 591a68b990bee9d874eaf90dd78b80cdf148876a Mon Sep 17 00:00:00 2001 From: develric Date: Tue, 24 Nov 2020 18:42:52 +0100 Subject: [PATCH 10/81] Adding API calls provider that provides calls to API for subscribe/unsubscribe to a post comments. --- .../utils/PostSubscribersApiCallsProvider.kt | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/utils/PostSubscribersApiCallsProvider.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/PostSubscribersApiCallsProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/PostSubscribersApiCallsProvider.kt new file mode 100644 index 000000000000..e172a644599f --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/PostSubscribersApiCallsProvider.kt @@ -0,0 +1,184 @@ +package org.wordpress.android.ui.reader.utils + +import com.android.volley.VolleyError +import com.wordpress.rest.RestRequest.ErrorListener +import com.wordpress.rest.RestRequest.Listener +import org.json.JSONObject +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.ui.reader.utils.PostSubscribersApiCallsProvider.PostSubscribersCallResult.Failure +import org.wordpress.android.ui.reader.utils.PostSubscribersApiCallsProvider.PostSubscribersCallResult.Success +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T +import org.wordpress.android.util.VolleyUtils +import org.wordpress.android.viewmodel.ContextProvider +import javax.inject.Inject +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume + +class PostSubscribersApiCallsProvider @Inject constructor( + private val contextProvider: ContextProvider +) { + fun getCanFollowComments(blogId: Long, cont: Continuation) { + val endPointPath = "/sites/$blogId/" + + val listener = Listener { jsonObject -> + val result = canFollowComments(blogId, jsonObject) + AppLog.d( + T.READER, + "getCanFollowComments > Succeeded [blogId=$blogId - result = $result]" + ) + cont.resume(result is Success) + } + val errorListener = ErrorListener { volleyError -> + AppLog.d( + T.READER, + "getCanFollowComments > Failed [blogId=$blogId - volleyError = $volleyError]" + ) + cont.resume(false) + } + + WordPress.getRestClientUtilsV1_1().get( + endPointPath, + listener, + errorListener + ) + } + + fun getMySubscriptionToPost(blogId: Long, postId: Long, cont: Continuation) { + val endPointPath = "/sites/$blogId/posts/$postId/subscribers/mine" + + val listener = Listener { jsonObject -> + val result = isFollowing(jsonObject) + AppLog.d( + T.READER, + "getMySubscriptionToPost > Succeeded [blogId=$blogId - postId=$postId - result = $result]" + ) + cont.resume(result) + } + val errorListener = ErrorListener { volleyError -> + val error = getErrorStringAndLog("getMySubscriptionToPost", blogId, postId, volleyError) + cont.resume(Failure(error)) + } + + WordPress.getRestClientUtilsV1_1().get( + endPointPath, + listener, + errorListener + ) + } + + fun subscribeMeToPost(blogId: Long, postId: Long, cont: Continuation) { + val endPointPath = "/sites/$blogId/posts/$postId/subscribers/new" + + val listener = Listener { jsonObject -> + val result = wasSubscribed(jsonObject) + AppLog.d( + T.READER, + "subscribeMeToPost > Succeeded [blogId=$blogId - postId=$postId - result = $result]" + ) + cont.resume(result) + } + val errorListener = ErrorListener { volleyError -> + val error = getErrorStringAndLog("subscribeMeToPost", blogId, postId, volleyError) + cont.resume(Failure(error)) + } + + WordPress.getRestClientUtilsV1_1().post( + endPointPath, + listener, + errorListener + ) + } + + fun unsubscribeMeFromPost(blogId: Long, postId: Long, cont: Continuation) { + val endPointPath = "/sites/$blogId/posts/$postId/subscribers/mine/delete" + + val listener = Listener { jsonObject -> + val result = wasUnsubscribed(jsonObject) + AppLog.d( + T.READER, + "unsubscribeMeFromPost > Succeeded [blogId=$blogId - postId=$postId - result = $result]" + ) + cont.resume(result) + } + val errorListener = ErrorListener { volleyError -> + val error = getErrorStringAndLog("unsubscribeMeFromPost", blogId, postId, volleyError) + cont.resume(Failure(error)) + } + + WordPress.getRestClientUtilsV1_1().post( + endPointPath, + listener, + errorListener + ) + } + + private fun getErrorStringAndLog(functionName: String, blogId: Long, postId: Long, volleyError: VolleyError?): String { + var error = VolleyUtils.errStringFromVolleyError(volleyError) + return if (error.isNullOrEmpty()) { + AppLog.d( + T.READER, + "functionName > Failed with empty string [blogId=$blogId - postId=$postId - volleyError = $volleyError]" + ) + contextProvider.getContext().getString(R.string.reader_follow_comments_get_status_error, postId) + } else { + AppLog.d( + T.READER, + "functionName > Failed [blogId=$blogId - postId=$postId - error = $error]" + ) + error + } + } + + private fun isFollowing(json: JSONObject?): PostSubscribersCallResult { + return json?.let { + if (it.has("i_subscribe")) { + Success(it.optBoolean("i_subscribe", false)) + } else { + Failure(contextProvider.getContext().getString(R.string.reader_follow_comments_bad_format_response)) + } + } ?: Failure(contextProvider.getContext().getString(R.string.reader_follow_comments_null_response)) + } + + private fun canFollowComments(blogId: Long, json: JSONObject?): PostSubscribersCallResult { + return json?.let { + if (it.has("ID") && it.optLong( "ID", -1) == blogId) { + Success(false) + } else { + Failure(contextProvider.getContext().getString(R.string.reader_follow_comments_bad_format_response)) + } + } ?: Failure(contextProvider.getContext().getString(R.string.reader_follow_comments_null_response)) + } + + private fun wasSubscribed(json: JSONObject?): PostSubscribersCallResult { + return json?.let { + val success = it.optBoolean("success", false) + val subscribed = it.optBoolean("i_subscribe", false) + + if (success && subscribed) { + Success(true) + } else { + Failure(contextProvider.getContext().getString(R.string.reader_follow_comments_bad_format_response)) + } + } ?: Failure(contextProvider.getContext().getString(R.string.reader_follow_comments_null_response)) + } + + private fun wasUnsubscribed(json: JSONObject?): PostSubscribersCallResult { + return json?.let { + val success = it.optBoolean("success", false) + val subscribed = it.optBoolean("i_subscribe", true) + + if (success && !subscribed) { + Success(false) + } else { + Failure(contextProvider.getContext().getString(R.string.reader_follow_comments_bad_format_response)) + } + } ?: Failure(contextProvider.getContext().getString(R.string.reader_follow_comments_null_response)) + } + + sealed class PostSubscribersCallResult { + data class Success(val isFollowing: Boolean) : PostSubscribersCallResult() + data class Failure(val error: String) : PostSubscribersCallResult() + } +} From 32bed174b7d60dabc1181082cfc4d289793029c7 Mon Sep 17 00:00:00 2001 From: develric Date: Tue, 24 Nov 2020 18:46:07 +0100 Subject: [PATCH 11/81] Making ReaderFollowButton configurable to support FOLLOW_SITE and FOLLOW_COMMENTS type. --- .../ui/reader/views/ReaderFollowButton.kt | 19 ++++++++-- .../ui/reader/views/ReaderFollowButtonType.kt | 37 +++++++++++++++++++ .../secondary_gray_20_disabled_selector.xml | 7 ++++ WordPress/src/main/res/values/attrs.xml | 4 ++ 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButtonType.kt create mode 100644 WordPress/src/main/res/color/secondary_gray_20_disabled_selector.xml diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.kt index a650764ba3f1..adfd861d7dfb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.kt @@ -12,6 +12,7 @@ import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import com.google.android.material.button.MaterialButton import org.wordpress.android.R +import org.wordpress.android.ui.reader.views.ReaderFollowButtonType.FOLLOW_SITE /** * Follow button used in reader detail @@ -23,6 +24,7 @@ class ReaderFollowButton @JvmOverloads constructor( ) : MaterialButton(context, attrs, defStyleAttr) { private var isFollowed = false private var showCaption = false + private var followButtonType = FOLLOW_SITE init { initView(context, attrs) @@ -34,10 +36,21 @@ class ReaderFollowButton @JvmOverloads constructor( attrs?.let { val array = context.theme.obtainStyledAttributes(attrs, R.styleable.ReaderFollowButton, 0, 0) showCaption = array.getBoolean(R.styleable.ReaderFollowButton_wpShowFollowButtonCaption, true) + + try { + val buttonTypeValue = array.getInteger(R.styleable.ReaderFollowButton_wpReaderFollowButtonType, -1) + if (buttonTypeValue != -1) { + followButtonType = ReaderFollowButtonType.fromInt(buttonTypeValue) + } + } finally { + array.recycle() + } } if (!showCaption) { hideCaptionAndEnlargeIcon(context) } + + updateFollowTextAndIcon() } private fun hideCaptionAndEnlargeIcon(context: Context) { @@ -49,13 +62,13 @@ class ReaderFollowButton @JvmOverloads constructor( private fun updateFollowTextAndIcon() { if (showCaption) { - setText(if (isFollowed) R.string.reader_btn_unfollow else R.string.reader_btn_follow) + setText(if (isFollowed) followButtonType.captionFollowing else followButtonType.captionFollow) } isSelected = isFollowed val drawableId = if (isFollowed) { - R.drawable.ic_reader_following_white_24dp + followButtonType.iconFollowing } else { - R.drawable.ic_reader_follow_white_24dp + followButtonType.iconFollow } icon = context.resources.getDrawable(drawableId, context.theme) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButtonType.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButtonType.kt new file mode 100644 index 000000000000..d4a2a558c3d8 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButtonType.kt @@ -0,0 +1,37 @@ +package org.wordpress.android.ui.reader.views + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import org.wordpress.android.R + +enum class ReaderFollowButtonType( + val value: Int, + @StringRes val captionFollow: Int, + @StringRes val captionFollowing: Int, + @DrawableRes val iconFollow: Int, + @DrawableRes val iconFollowing: Int +) { + FOLLOW_SITE( + 0, + R.string.reader_btn_follow, + R.string.reader_btn_unfollow, + R.drawable.ic_reader_follow_white_24dp, + R.drawable.ic_reader_following_white_24dp + ), + // Note: even though AS does not catch it and it says it is not used, FOLLOW_COMMENTS is actually used currently + // by fromInt function to evaluate wpReaderFollowButtonType attr for example in the ReaderFollowButton that + // is placed in reader_comments_post_header_view.xml. Mind of this before to remove! + FOLLOW_COMMENTS( + 1, + R.string.reader_btn_follow_comments, + R.string.reader_btn_following_comments, + R.drawable.ic_reader_follow_conversation_white_24dp, + R.drawable.ic_reader_following_conversation_white_24dp + ); + + companion object { + fun fromInt(value: Int): ReaderFollowButtonType = + values().firstOrNull { it.value == value } + ?: throw IllegalArgumentException("ReaderFollowButtonType wrong value $value") + } +} diff --git a/WordPress/src/main/res/color/secondary_gray_20_disabled_selector.xml b/WordPress/src/main/res/color/secondary_gray_20_disabled_selector.xml new file mode 100644 index 000000000000..0f875feac88a --- /dev/null +++ b/WordPress/src/main/res/color/secondary_gray_20_disabled_selector.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/WordPress/src/main/res/values/attrs.xml b/WordPress/src/main/res/values/attrs.xml index 5b60ff8cec1d..5b2a257b9aa3 100644 --- a/WordPress/src/main/res/values/attrs.xml +++ b/WordPress/src/main/res/values/attrs.xml @@ -101,6 +101,10 @@ ReaderFollowButton attributes --> + + + + From 0fe3ef30abc41ba1d6fd8afd33d760b690e2bd4a Mon Sep 17 00:00:00 2001 From: develric Date: Tue, 24 Nov 2020 18:47:16 +0100 Subject: [PATCH 12/81] Adding string resources for ReaderFollowButton and UI messages. --- WordPress/src/main/res/values/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index bbe75e919e93..f3c93dc28ea7 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1342,6 +1342,12 @@ Try following more topics to broaden the search Get Started Follow topics + Null response received + Bad format response received + Could not retrieve site. Follow comments disabled + Successfully subscribed to the comments + Successfully unsubscribed from the comments + Error fetching subscription status for post: %d Post saved online @@ -1742,7 +1748,8 @@ Turn on site notifications Select a few to continue Done - + Follow conversation + Following conversation Remove the current filter From d0aa3be2375e6eae66ebaadaac524ac0a0c547f0 Mon Sep 17 00:00:00 2001 From: develric Date: Tue, 24 Nov 2020 18:47:55 +0100 Subject: [PATCH 13/81] Adding custom style for follow/unfollow comments button. --- WordPress/src/main/res/values/reader_styles.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/WordPress/src/main/res/values/reader_styles.xml b/WordPress/src/main/res/values/reader_styles.xml index 17b8887f80c4..d85d7f44e850 100644 --- a/WordPress/src/main/res/values/reader_styles.xml +++ b/WordPress/src/main/res/values/reader_styles.xml @@ -265,6 +265,17 @@ false + +