diff --git a/.circleci/config.yml b/.circleci/config.yml index 1b3bbeadbae7..bd9b0ae96cbf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -105,7 +105,7 @@ jobs: - restore-gutenberg-bundle-cache - run: name: Ensure assets folder exists - command: mkdir libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets - attach_workspace: at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets - run: @@ -132,7 +132,7 @@ jobs: - restore-gutenberg-bundle-cache - run: name: Ensure assets folder exists - command: mkdir libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets - attach_workspace: at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets - run: @@ -179,7 +179,7 @@ jobs: - restore-gutenberg-bundle-cache - run: name: Ensure assets folder exists - command: mkdir libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets - attach_workspace: at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets - run: @@ -224,7 +224,7 @@ jobs: - restore-gutenberg-bundle-cache - run: name: Ensure assets folder exists - command: mkdir libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets - attach_workspace: at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets - run: @@ -267,7 +267,7 @@ jobs: - restore-gutenberg-bundle-cache - run: name: Ensure assets folder exists - command: mkdir libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets - attach_workspace: at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets - run: diff --git a/README.md b/README.md index b85532c265e3..3220dc749185 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ it on [Google Play](https://play.google.com/store/apps/details?id=org.wordpress. Notes: * To use WordPress.com features (login to WordPress.com, access Reader and Stats, etc) you need a WordPress.com OAuth2 ID and secret. Please read the [OAuth2 Authentication](#oauth2-authentication) section. +* While loading/building the app in Android Studio ignore the prompt to update the gradle plugin version as that will probably introduce build errors. On the other hand, feel free to update if you are planning to work on ensuring the compatibility of the newer version. ## OAuth2 Authentication ## diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 474253cc8369..62061087606b 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,6 +1,13 @@ -15.1 +15.2 ----- +15.1 +----- +* Fixes issue on Notifications tab when two screens were drawn on top of each other +* [**] Fix video thumbnails, settings and preview in Media section for private sites +* [**] Block Editor: Adds editor support for theme defined colors and theme defined gradients on cover and button blocks. +* [*] Support for breaking out of captions/citation authors by pressing enter on the following blocks: image, video, gallery, quote, and pullquote. + 15.0 ----- * [*] Fix wrong icon is used when a Password is visible diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 46d4c0ffff72..faca61addfe9 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -55,9 +55,9 @@ android { if (project.hasProperty("versionName")) { versionName project.property("versionName") } else { - versionName "alpha-228" + versionName "alpha-229" } - versionCode 879 + versionCode 882 minSdkVersion rootProject.minSdkVersion targetSdkVersion rootProject.targetSdkVersion @@ -85,9 +85,9 @@ android { dimension "buildType" // Only set the release version if one isn't provided if (!project.hasProperty("versionName")) { - versionName "15.0" + versionName "15.1-rc-1" } - versionCode 880 + versionCode 881 buildConfigField "boolean", "ME_ACTIVITY_AVAILABLE", "false" buildConfigField "boolean", "TENOR_AVAILABLE", "false" buildConfigField "boolean", "READER_IMPROVEMENTS_PHASE_2", "false" @@ -252,8 +252,7 @@ dependencies { exclude group: 'com.android.support', module: 'support-v4' exclude module: 'recyclerview-v7' } - androidTestImplementation('com.github.tomakehurst:wiremock:2.23.2') { - exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' + androidTestImplementation('com.github.tomakehurst:wiremock:2.26.3') { exclude group: 'org.apache.httpcomponents', module: 'httpclient' exclude group: 'org.apache.commons', module: 'commons-lang3' exclude group: 'asm', module: 'asm' diff --git a/WordPress/src/main/java/org/wordpress/android/WordPress.java b/WordPress/src/main/java/org/wordpress/android/WordPress.java index 56cb9d7e59e9..1021996f6e03 100644 --- a/WordPress/src/main/java/org/wordpress/android/WordPress.java +++ b/WordPress/src/main/java/org/wordpress/android/WordPress.java @@ -90,7 +90,7 @@ import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.AppThemeUtils; import org.wordpress.android.util.BitmapLruCache; -import org.wordpress.android.util.CrashLoggingUtils; +import org.wordpress.android.util.CrashLogging; import org.wordpress.android.util.DateTimeUtils; import org.wordpress.android.util.FluxCUtils; import org.wordpress.android.util.LocaleManager; @@ -161,6 +161,7 @@ public class WordPress extends MultiDexApplication implements HasServiceInjector @Inject ImageManager mImageManager; @Inject PrivateAtomicCookie mPrivateAtomicCookie; @Inject ImageEditorTracker mImageEditorTracker; + @Inject CrashLogging mCrashLogging; // For development and production `AnalyticsTrackerNosara`, for testing a mocked `Tracker` will be injected. @Inject Tracker mTracker; @@ -230,10 +231,6 @@ public void onCreate() { // This call needs be made before accessing any methods in android.webkit package setWebViewDataDirectorySuffixOnAndroidP(); - if (CrashLoggingUtils.shouldEnableCrashLogging(getContext())) { - CrashLoggingUtils.startCrashLogging(getContext()); - } - initWellSql(); // Init Dagger @@ -241,6 +238,8 @@ public void onCreate() { component().inject(this); mDispatcher.register(this); + mCrashLogging.start(getContext()); + // Init static fields from dagger injected singletons, for legacy Actions and Utilities sRequestQueue = mRequestQueue; sImageLoader = mImageLoader; @@ -257,7 +256,7 @@ public void onLog(T tag, LogLevel logLevel, String message) { StringBuffer sb = new StringBuffer(); sb.append(logLevel.toString()).append("/").append(AppLog.TAG).append("-") .append(tag.toString()).append(": ").append(message); - CrashLoggingUtils.log(sb.toString()); + mCrashLogging.log(sb.toString()); } }); AppLog.i(T.UTILS, "WordPress.onCreate"); @@ -862,7 +861,6 @@ public void onAppGoesToBackground() { AppLog.d(T.MAIN, "ConnectionChangeReceiver successfully unregistered"); } catch (IllegalArgumentException e) { AppLog.e(T.MAIN, "ConnectionChangeReceiver was already unregistered"); - CrashLoggingUtils.log(e); } } } diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java index 45db2efa0b56..a36da83a2a64 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java +++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java @@ -21,7 +21,6 @@ import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId; import org.wordpress.android.ui.reader.models.ReaderBlogIdPostIdList; import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.CrashLoggingUtils; import org.wordpress.android.util.SqlUtils; import java.util.Locale; @@ -1096,7 +1095,6 @@ private static ReaderPostList getPostListFromCursor(Cursor cursor) { } while (cursor.moveToNext()); } } catch (IllegalStateException e) { - CrashLoggingUtils.log(e); AppLog.e(AppLog.T.READER, e); } return posts; diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java index 93c02db43da2..1f3604038648 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -90,12 +90,20 @@ import org.wordpress.android.ui.posts.PostDatePickerDialogFragment; import org.wordpress.android.ui.posts.PostListFragment; import org.wordpress.android.ui.posts.PostNotificationScheduleTimeDialogFragment; -import org.wordpress.android.ui.posts.PostSettingsTagsActivity; +import org.wordpress.android.ui.posts.PostSettingsTagsFragment; import org.wordpress.android.ui.posts.PostTimePickerDialogFragment; import org.wordpress.android.ui.posts.PostsListActivity; +import org.wordpress.android.ui.posts.PrepublishingHomeAdapter; +import org.wordpress.android.ui.posts.PrepublishingHomeFragment; +import org.wordpress.android.ui.posts.PrepublishingBottomSheetFragment; +import org.wordpress.android.ui.posts.PrepublishingTagsFragment; import org.wordpress.android.ui.posts.PublishNotificationReceiver; import org.wordpress.android.ui.posts.SelectCategoriesActivity; import org.wordpress.android.ui.posts.adapters.AuthorSelectionAdapter; +import org.wordpress.android.ui.posts.services.AztecVideoLoader; +import org.wordpress.android.ui.posts.prepublishing.PrepublishingPublishSettingsFragment; +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityAdapter; +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityFragment; import org.wordpress.android.ui.prefs.AccountSettingsFragment; import org.wordpress.android.ui.prefs.AppSettingsActivity; import org.wordpress.android.ui.prefs.AppSettingsFragment; @@ -131,6 +139,8 @@ import org.wordpress.android.ui.reader.adapters.ReaderCommentAdapter; import org.wordpress.android.ui.reader.adapters.ReaderPostAdapter; import org.wordpress.android.ui.reader.adapters.ReaderUserAdapter; +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsFragment; +import org.wordpress.android.ui.reader.discover.ReaderDiscoverFragment; import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic; import org.wordpress.android.ui.reader.views.ReaderLikingUsersView; import org.wordpress.android.ui.reader.views.ReaderSiteHeaderView; @@ -162,6 +172,7 @@ import org.wordpress.android.ui.uploads.UploadService; import org.wordpress.android.ui.whatsnew.FeatureAnnouncementDialogFragment; import org.wordpress.android.ui.whatsnew.FeatureAnnouncementListAdapter; +import org.wordpress.android.util.CrashLogging; import org.wordpress.android.util.HtmlToSpannedConverter; import org.wordpress.android.util.WPWebViewClient; import org.wordpress.android.util.image.getters.WPCustomImageGetter; @@ -329,8 +340,6 @@ public interface AppComponent extends AndroidInjector { void inject(EditPostSettingsFragment object); - void inject(PostSettingsTagsActivity object); - void inject(PostsListActivity object); void inject(AuthorSelectionAdapter object); @@ -497,6 +506,22 @@ public interface AppComponent extends AndroidInjector { void inject(PageParentSearchFragment object); + void inject(PrepublishingBottomSheetFragment object); + + void inject(PrepublishingHomeFragment object); + + void inject(PrepublishingHomeAdapter object); + + void inject(PrepublishingTagsFragment object); + + void inject(PostSettingsTagsFragment object); + + void inject(PrepublishingPublishSettingsFragment object); + + void inject(PrepublishingVisibilityFragment object); + + void inject(PrepublishingVisibilityAdapter object); + void inject(AppSettingsActivity object); void inject(FeatureAnnouncementDialogFragment object); @@ -505,10 +530,18 @@ public interface AppComponent extends AndroidInjector { void inject(ReaderFragment object); + void inject(ReaderDiscoverFragment object); + void inject(ReaderSearchActivity object); + void inject(ReaderInterestsFragment object); + void inject(HomepageSettingsDialog object); + void inject(CrashLogging object); + + void inject(AztecVideoLoader object); + // Allows us to inject the application without having to instantiate any modules, and provides the Application // in the app graph @Component.Builder diff --git a/WordPress/src/main/java/org/wordpress/android/modules/SupportModule.kt b/WordPress/src/main/java/org/wordpress/android/modules/SupportModule.kt index 830b629e1a96..1e94677ad636 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/SupportModule.kt +++ b/WordPress/src/main/java/org/wordpress/android/modules/SupportModule.kt @@ -6,6 +6,8 @@ import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.support.SupportHelper import org.wordpress.android.support.ZendeskHelper +import org.wordpress.android.support.ZendeskPlanFieldHelper +import org.wordpress.android.util.CrashLogging import javax.inject.Singleton @Module @@ -15,10 +17,16 @@ class SupportModule { fun provideZendeskHelper( accountStore: AccountStore, siteStore: SiteStore, - supportHelper: SupportHelper - ): ZendeskHelper = ZendeskHelper(accountStore, siteStore, supportHelper) + supportHelper: SupportHelper, + zendeskPlanFieldHelper: ZendeskPlanFieldHelper + ): ZendeskHelper = ZendeskHelper(accountStore, siteStore, supportHelper, zendeskPlanFieldHelper) @Singleton @Provides fun provideSupportHelper(): SupportHelper = SupportHelper() + + @Singleton + @Provides + fun provideZendeskPlanFieldHelper(remoteLoggingUtils: CrashLogging): ZendeskPlanFieldHelper = + ZendeskPlanFieldHelper(remoteLoggingUtils) } diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java index 2cf7716428d0..19032d11415f 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java @@ -5,12 +5,20 @@ import org.wordpress.android.ui.JetpackRemoteInstallViewModel; import org.wordpress.android.ui.domains.DomainRegistrationMainViewModel; +import org.wordpress.android.ui.main.MeViewModel; import org.wordpress.android.ui.plans.PlansViewModel; import org.wordpress.android.ui.posts.EditPostPublishSettingsViewModel; import org.wordpress.android.ui.posts.PostListMainViewModel; +import org.wordpress.android.ui.posts.PrepublishingHomeViewModel; +import org.wordpress.android.ui.posts.PrepublishingTagsViewModel; +import org.wordpress.android.ui.posts.PrepublishingViewModel; import org.wordpress.android.ui.posts.editor.StorePostViewModel; +import org.wordpress.android.ui.posts.prepublishing.PrepublishingPublishSettingsViewModel; +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityViewModel; import org.wordpress.android.ui.prefs.homepage.HomepageSettingsViewModel; import org.wordpress.android.ui.reader.ReaderCommentListViewModel; +import org.wordpress.android.ui.reader.discover.ReaderDiscoverViewModel; +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel; import org.wordpress.android.ui.reader.subfilter.SubFilterViewModel; import org.wordpress.android.ui.reader.viewmodels.NewsCardViewModel; import org.wordpress.android.ui.reader.viewmodels.ReaderPostListViewModel; @@ -302,6 +310,16 @@ abstract class ViewModelModule { @ViewModelKey(ReaderViewModel.class) abstract ViewModel readerParentPostListViewModel(ReaderViewModel viewModel); + @Binds + @IntoMap + @ViewModelKey(ReaderDiscoverViewModel.class) + abstract ViewModel readerDiscoverViewModel(ReaderDiscoverViewModel viewModel); + + @Binds + @IntoMap + @ViewModelKey(ReaderInterestsViewModel.class) + abstract ViewModel readerInterestsViewModel(ReaderInterestsViewModel viewModel); + @Binds @IntoMap @ViewModelKey(NewsCardViewModel.class) @@ -312,6 +330,36 @@ abstract class ViewModelModule { @ViewModelKey(HomepageSettingsViewModel.class) abstract ViewModel homepageSettingsDialogViewModel(HomepageSettingsViewModel viewModel); + @Binds + @IntoMap + @ViewModelKey(PrepublishingViewModel.class) + abstract ViewModel prepublishingViewModel(PrepublishingViewModel viewModel); + + @Binds + @IntoMap + @ViewModelKey(PrepublishingHomeViewModel.class) + abstract ViewModel prepublishingOptionsViewModel(PrepublishingHomeViewModel viewModel); + + @Binds + @IntoMap + @ViewModelKey(PrepublishingTagsViewModel.class) + abstract ViewModel prepublishingTagsViewModel(PrepublishingTagsViewModel viewModel); + + @Binds + @IntoMap + @ViewModelKey(PrepublishingPublishSettingsViewModel.class) + abstract ViewModel prepublishingPublishSettingsViewModel(PrepublishingPublishSettingsViewModel viewModel); + + @Binds + @IntoMap + @ViewModelKey(PrepublishingVisibilityViewModel.class) + abstract ViewModel prepublishingVisibilityViewModel(PrepublishingVisibilityViewModel viewModel); + + @Binds + @IntoMap + @ViewModelKey(MeViewModel.class) + abstract ViewModel meViewModel(MeViewModel viewModel); + @Binds abstract ViewModelProvider.Factory provideViewModelFactory(ViewModelFactory viewModelFactory); } diff --git a/WordPress/src/main/java/org/wordpress/android/support/ZendeskHelper.kt b/WordPress/src/main/java/org/wordpress/android/support/ZendeskHelper.kt index 11888eae0745..bc8dc0036ba6 100644 --- a/WordPress/src/main/java/org/wordpress/android/support/ZendeskHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/support/ZendeskHelper.kt @@ -46,7 +46,8 @@ private const val enablePushNotificationsDelayAfterIdentityChange: Long = 2500 class ZendeskHelper( private val accountStore: AccountStore, private val siteStore: SiteStore, - private val supportHelper: SupportHelper + private val supportHelper: SupportHelper, + private val zendeskPlanFieldHelper: ZendeskPlanFieldHelper ) { private val zendeskInstance: Zendesk get() = Zendesk.INSTANCE @@ -124,7 +125,17 @@ class ZendeskHelper( .withShowConversationsMenuButton(isIdentitySet) AnalyticsTracker.track(Stat.SUPPORT_HELP_CENTER_VIEWED) if (isIdentitySet) { - builder.show(context, buildZendeskConfig(context, siteStore.sites, origin, selectedSite, extraTags)) + builder.show( + context, + buildZendeskConfig( + context, + siteStore.sites, + origin, + selectedSite, + extraTags, + zendeskPlanFieldHelper + ) + ) } else { builder.show(context) } @@ -148,7 +159,17 @@ class ZendeskHelper( requireIdentity(context, selectedSite) { AnalyticsTracker.track(Stat.SUPPORT_NEW_REQUEST_VIEWED) RequestActivity.builder() - .show(context, buildZendeskConfig(context, siteStore.sites, origin, selectedSite, extraTags)) + .show( + context, + buildZendeskConfig( + context, + siteStore.sites, + origin, + selectedSite, + extraTags, + zendeskPlanFieldHelper + ) + ) } } @@ -169,7 +190,17 @@ class ZendeskHelper( requireIdentity(context, selectedSite) { AnalyticsTracker.track(Stat.SUPPORT_TICKET_LIST_VIEWED) RequestListActivity.builder() - .show(context, buildZendeskConfig(context, siteStore.sites, origin, selectedSite, extraTags)) + .show( + context, + buildZendeskConfig( + context, + siteStore.sites, + origin, + selectedSite, + extraTags, + zendeskPlanFieldHelper + ) + ) } } @@ -342,13 +373,17 @@ private fun buildZendeskConfig( allSites: List?, origin: Origin?, selectedSite: SiteModel? = null, - extraTags: List? = null + extraTags: List? = null, + zendeskPlanFieldHelper: ZendeskPlanFieldHelper ): UiConfig { return RequestActivity.builder() - .withTicketForm(TicketFieldIds.form, buildZendeskCustomFields(context, allSites, selectedSite)) - .withRequestSubject(ZendeskConstants.ticketSubject) - .withTags(buildZendeskTags(allSites, origin ?: Origin.UNKNOWN, extraTags)) - .config() + .withTicketForm( + TicketFieldIds.form, + buildZendeskCustomFields(context, allSites, selectedSite, zendeskPlanFieldHelper) + ) + .withRequestSubject(ZendeskConstants.ticketSubject) + .withTags(buildZendeskTags(allSites, origin ?: Origin.UNKNOWN, extraTags)) + .config() } /** @@ -358,7 +393,8 @@ private fun buildZendeskConfig( private fun buildZendeskCustomFields( context: Context, allSites: List?, - selectedSite: SiteModel? + selectedSite: SiteModel?, + zendeskPlanFieldHelper: ZendeskPlanFieldHelper ): List { val currentSiteInformation = if (selectedSite != null) { "${SiteUtils.getHomeURLOrHostName(selectedSite)} (${selectedSite.stateLogInformation})" @@ -366,16 +402,25 @@ private fun buildZendeskCustomFields( "not_selected" } - return listOf( - CustomField(TicketFieldIds.appVersion, PackageUtils.getVersionName(context)), - CustomField(TicketFieldIds.blogList, getCombinedLogInformationOfSites(allSites)), - CustomField(TicketFieldIds.currentSite, currentSiteInformation), - CustomField(TicketFieldIds.deviceFreeSpace, DeviceUtils.getTotalAvailableMemorySize()), - CustomField(TicketFieldIds.logs, AppLog.toPlainText(context)), - CustomField(TicketFieldIds.networkInformation, getNetworkInformation(context)), - CustomField(TicketFieldIds.appLanguage, LanguageUtils.getPatchedCurrentDeviceLanguage(context)), - CustomField(TicketFieldIds.sourcePlatform, ZendeskConstants.sourcePlatform) + val customFields = arrayListOf( + CustomField(TicketFieldIds.appVersion, PackageUtils.getVersionName(context)), + CustomField(TicketFieldIds.blogList, getCombinedLogInformationOfSites(allSites)), + CustomField(TicketFieldIds.currentSite, currentSiteInformation), + CustomField(TicketFieldIds.deviceFreeSpace, DeviceUtils.getTotalAvailableMemorySize()), + CustomField(TicketFieldIds.logs, AppLog.toPlainText(context)), + CustomField(TicketFieldIds.networkInformation, getNetworkInformation(context)), + CustomField(TicketFieldIds.appLanguage, LanguageUtils.getPatchedCurrentDeviceLanguage(context)), + CustomField(TicketFieldIds.sourcePlatform, ZendeskConstants.sourcePlatform) ) + allSites?.let { + val planIds = it.map { site -> site.planId }.distinct() + val highestPlan = zendeskPlanFieldHelper.getHighestPlan(planIds) + if (highestPlan != UNKNOWN_PLAN) { + customFields.add(CustomField(TicketFieldIds.highestPlan, highestPlan)) + } + } + + return customFields } /** @@ -429,10 +474,6 @@ private fun buildZendeskTags(allSites: List?, origin: Origin, extraTa if (it.any { site -> site.isJetpackConnected }) { tags.add(ZendeskConstants.jetpackTag) } - - // Find distinct plans and add them - val plans = it.mapNotNull { site -> site.planShortName }.distinct() - tags.addAll(plans) } tags.add(origin.toString()) // Add Android tag to make it easier to filter tickets by platform @@ -498,6 +539,7 @@ private object TicketFieldIds { const val currentSite = 360000103103L const val appLanguage = 360008583691L const val sourcePlatform = 360009311651L + const val highestPlan = 25175963L } object ZendeskExtraTags { diff --git a/WordPress/src/main/java/org/wordpress/android/support/ZendeskPlanFieldHelper.kt b/WordPress/src/main/java/org/wordpress/android/support/ZendeskPlanFieldHelper.kt new file mode 100644 index 000000000000..c59811c1d304 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/support/ZendeskPlanFieldHelper.kt @@ -0,0 +1,155 @@ +package org.wordpress.android.support + +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T.SUPPORT +import org.wordpress.android.util.CrashLogging + +const val ZENDESK_UNKNOWN_PLAN_IDS_ERROR = "See issue #12064; zendesk-unknown-plan-ids" +const val UNKNOWN_PLAN = "unknown-plan" + +class ZendeskPlanFieldHelper(private val remoteLoggingUtils: CrashLogging) { + // wpcom plans + private val wpComEcommercePlans = listOf( + WpComPlansConstants.WPCOM_ECOMMERCE_BUNDLE, + WpComPlansConstants.WPCOM_ECOMMERCE_BUNDLE_2Y + ) + + private val wpComBusinessPlans = listOf( + WpComPlansConstants.WPCOM_BUSINESS_BUNDLE, + WpComPlansConstants.WPCOM_BUSINESS_BUNDLE_MONTHLY, + WpComPlansConstants.WPCOM_BUSINESS_BUNDLE_2Y + ) + + private val wpComPremiumPlans = listOf( + WpComPlansConstants.WPCOM_PRO_BUNDLE, + WpComPlansConstants.WPCOM_VALUE_BUNDLE, + WpComPlansConstants.WPCOM_VALUE_BUNDLE_MONTHLY, + WpComPlansConstants.WPCOM_VALUE_BUNDLE_2Y + ) + + private val wpComPersonalPlans = listOf( + WpComPlansConstants.WPCOM_PERSONAL_BUNDLE, + WpComPlansConstants.WPCOM_PERSONAL_BUNDLE_2Y + ) + + private val wpComBloggerPlans = listOf( + WpComPlansConstants.WPCOM_BLOGGER_BUNDLE, + WpComPlansConstants.WPCOM_BLOGGER_BUNDLE_2Y + ) + + private val wpFreePlans = listOf( + WpComPlansConstants.WPCOM_FREE + ) + + // jetpack plans + private val jetpackBusinessPlans = listOf( + JetpackPlansConstants.JETPACK_BUSINESS, + JetpackPlansConstants.JETPACK_BUSINESS_MONTHLY + ) + + private val jetpackPremiumPlans = listOf( + JetpackPlansConstants.JETPACK_PREMIUM, + JetpackPlansConstants.JETPACK_PREMIUM_MONTHLY + ) + + private val jetpackPersonalPlans = listOf( + JetpackPlansConstants.JETPACK_PERSONAL, + JetpackPlansConstants.JETPACK_PERSONAL_MONTHLY + ) + + private val jetpackFreePlans = listOf( + JetpackPlansConstants.JETPACK_FREE + ) + + private val ecommercePlans = wpComEcommercePlans + private val businessOrProfessionalPlans = wpComBusinessPlans + jetpackBusinessPlans + private val premiumPlans = wpComPremiumPlans + jetpackPremiumPlans + private val personalPlans = wpComPersonalPlans + jetpackPersonalPlans + private val bloggerPlans = wpComBloggerPlans + private val freePlans = wpFreePlans + jetpackFreePlans + private val allPlans = ecommercePlans + + businessOrProfessionalPlans + + premiumPlans + + personalPlans + + bloggerPlans + + freePlans + + private fun getUnknownPlanIds(planIds: List) = planIds.subtract(allPlans) + + /** + * This is a helper function that checks plan types from most expensive to least, + * so that we return the highest value plan type for the user and give them the appropriate + * service level (in case they have more than one plan). + * Internal Ref: p8wKgj-1eQ#comment-7475 + * + * It doesn't support add_on_plan, tier and other plans that are included in the zendesk plan dropdown. + */ + fun getHighestPlan(planIds: List): String { + if (getUnknownPlanIds(planIds).isNotEmpty()) { + val logMessage = "$ZENDESK_UNKNOWN_PLAN_IDS_ERROR ${getUnknownPlanIds(planIds)}" + + AppLog.e(SUPPORT, ZendeskPlanFieldHelper::class.java.simpleName + logMessage) + remoteLoggingUtils.reportException(NoSuchElementException(logMessage)) + } + + return when { + ecommercePlans.intersect(planIds).isNotEmpty() -> { + ZendeskPlanConstants.ECOMMERCE + } + businessOrProfessionalPlans.intersect(planIds).isNotEmpty() -> { + ZendeskPlanConstants.BUSINESS_PROFESSIONAL + } + premiumPlans.intersect(planIds).isNotEmpty() -> { + ZendeskPlanConstants.PREMIUM + } + personalPlans.intersect(planIds).isNotEmpty() -> { + ZendeskPlanConstants.PERSONAL + } + bloggerPlans.intersect(planIds).isNotEmpty() -> { + ZendeskPlanConstants.BLOGGER + } + freePlans.intersect(planIds).isNotEmpty() -> { + ZendeskPlanConstants.FREE + } + else -> { + UNKNOWN_PLAN + } + } + } +} + +object WpComPlansConstants { + const val WPCOM_FREE = 1L + const val WPCOM_BLOGGER_BUNDLE = 1010L + const val WPCOM_PERSONAL_BUNDLE = 1009L + const val WPCOM_VALUE_BUNDLE = 1003L + const val WPCOM_PRO_BUNDLE = 1004L + const val WPCOM_BUSINESS_BUNDLE = 1008L + const val WPCOM_ECOMMERCE_BUNDLE = 1011L + const val WPCOM_VALUE_BUNDLE_MONTHLY = 1013L + const val WPCOM_BUSINESS_BUNDLE_MONTHLY = 1018L + const val WPCOM_PERSONAL_BUNDLE_2Y = 1029L + const val WPCOM_VALUE_BUNDLE_2Y = 1023L + const val WPCOM_BUSINESS_BUNDLE_2Y = 1028L + const val WPCOM_ECOMMERCE_BUNDLE_2Y = 1031L + const val WPCOM_BLOGGER_BUNDLE_2Y = 1030L +} + +object JetpackPlansConstants { + const val JETPACK_FREE = 2002L + const val JETPACK_PREMIUM = 2000L + const val JETPACK_BUSINESS = 2001L + const val JETPACK_PERSONAL = 2005L + const val JETPACK_PREMIUM_MONTHLY = 2003L + const val JETPACK_BUSINESS_MONTHLY = 2004L + const val JETPACK_PERSONAL_MONTHLY = 2006L +} + +object ZendeskPlanConstants { + const val ECOMMERCE = "ecommerce" + const val BUSINESS_PROFESSIONAL = "business_professional" + const val PREMIUM = "premium" + const val PERSONAL = "personal" + const val BLOGGER = "blogger" + const val FREE = "free" +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java index 587eeaa11763..5da4ff020d06 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -302,6 +302,21 @@ public static void openEditorForSiteInNewStack(Context context, @NonNull SiteMod taskStackBuilder.startActivities(); } + + public static void openEditorForPostInNewStack(Context context, @NonNull SiteModel site, int localPostId) { + TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); + Intent mainActivityIntent = getMainActivityInNewStack(context); + + Intent editorIntent = new Intent(context, EditPostActivity.class); + editorIntent.putExtra(WordPress.SITE, site); + editorIntent.putExtra(EditPostActivity.EXTRA_POST_LOCAL_ID, localPostId); + editorIntent.putExtra(EditPostActivity.EXTRA_IS_PAGE, false); + + taskStackBuilder.addNextIntent(mainActivityIntent); + taskStackBuilder.addNextIntent(editorIntent); + taskStackBuilder.startActivities(); + } + /** * Opens the editor and passes the information needed for a reblog action * diff --git a/WordPress/src/main/java/org/wordpress/android/ui/DeepLinkingIntentReceiverActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/DeepLinkingIntentReceiverActivity.java index f463ad87547d..14e4f538c722 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/DeepLinkingIntentReceiverActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/DeepLinkingIntentReceiverActivity.java @@ -11,8 +11,10 @@ import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.fluxc.model.PostModel; import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.fluxc.store.AccountStore; +import org.wordpress.android.fluxc.store.PostStore; import org.wordpress.android.fluxc.store.SiteStore; import org.wordpress.android.ui.reader.ReaderActivityLauncher; import org.wordpress.android.util.AppLog; @@ -55,6 +57,7 @@ public class DeepLinkingIntentReceiverActivity extends LocaleAwareActivity { @Inject AccountStore mAccountStore; @Inject SiteStore mSiteStore; + @Inject PostStore mPostStore; @Override protected void onCreate(Bundle savedInstanceState) { @@ -74,6 +77,8 @@ protected void onCreate(Bundle savedInstanceState) { mInterceptedUri = uri.toString(); if (shouldOpenEditor(uri)) { handleOpenEditor(uri); + } else if (shouldOpenEditorFromDeepLink(host)) { + handleOpenEditorFromDeepLink(uri); } else if (shouldHandleTrackingUrl(uri)) { // There is only one handled tracking URL for now (open editor) handleOpenEditorFromTrackingUrl(uri); @@ -99,6 +104,11 @@ private boolean shouldOpenEditor(@NonNull Uri uri) { return shouldShow(uri, POST_PATH); } + private boolean shouldOpenEditorFromDeepLink(String host) { + // Match: wordpress://post/... + return host != null && host.equals(DEEP_LINK_HOST_POST); + } + private @Nullable Uri getRedirectUri(@NonNull Uri uri) { String redirectTo = uri.getQueryParameter(REDIRECT_TO_PARAM); if (redirectTo == null) { @@ -133,8 +143,84 @@ private void handleOpenEditorFromTrackingUrl(@NonNull Uri uri) { handleOpenEditor(redirectUri); } + /** + * Opens post editor for provided uri. If uri contains a site and a postId + * (e.g. https://wordpress.com/example.com/1231/), opens the post for editing, if available. + * If the uri only contains a site (e.g. https://wordpress.com/example.com/ ), opens a new post + * editor for that site, if available. + * Else opens the new post editor for currently selected site. + */ private void handleOpenEditor(@NonNull Uri uri) { - openEditorForSite(extractTargetHost(uri)); + List pathSegments = uri.getPathSegments(); + + if (pathSegments.size() < 3) { + // No postId in path, open new post editor for site + openEditorForSite(extractTargetHost(uri)); + return; + } + + // Match: https://wordpress.com/post/blogNameOrUrl/postId + String targetHost = pathSegments.get(1); + String targetPostId = pathSegments.get(2); + openEditorForSiteAndPost(targetHost, targetPostId); + } + + /** + * Opens post editor for provided uri. If uri contains a site and a postId + * (e.g. wordpress/post?blogId=798&postId=1231), opens the post for editing, if available. + * If the uri only contains a site (e.g. wordpress/post?blogId=798 ), opens a new post + * editor for that site, if available. + * Else opens the new post editor for currently selected site. + */ + private void handleOpenEditorFromDeepLink(@NonNull Uri uri) { + String blogId = uri.getQueryParameter("blogId"); + String postId = uri.getQueryParameter("postId"); + + if (blogId == null) { + // No blogId provided. Follow default behaviour: open a blank editor with the current selected site + ActivityLauncher.openEditorInNewStack(getContext()); + return; + } + + SiteModel site; + + Long siteId = parseAsLongOrNull(blogId); + if (siteId != null) { + // Blog id is a number so we check for it as site id + site = mSiteStore.getSiteBySiteId(siteId); + } else { + // Blog id is not a number so we check for it as blog name or url + List matchedSites = mSiteStore.getSitesByNameOrUrlMatching(blogId); + site = matchedSites.isEmpty() ? null : matchedSites.get(0); + } + + if (site == null) { + // Site not found. Open a blank editor with the current selected site + ToastUtils.showToast(getContext(), R.string.blog_not_found); + ActivityLauncher.openEditorInNewStack(getContext()); + return; + } + + Long remotePostId = parseAsLongOrNull(postId); + + if (remotePostId == null) { + // Open new post editor for given site + ActivityLauncher.openEditorForSiteInNewStack(getContext(), site); + return; + } + + // Check if post is available for opening + PostModel post = mPostStore.getPostByRemotePostId(remotePostId, site); + + if (post == null) { + ToastUtils.showToast(getContext(), R.string.post_not_found); + // Post not found. Open new post editor for given site. + ActivityLauncher.openEditorForSiteInNewStack(getContext(), site); + return; + } + + // Open editor with post + ActivityLauncher.openEditorForPostInNewStack(getContext(), site, post.getId()); } private void openEditorForSite(@NonNull String targetHost) { @@ -149,6 +235,41 @@ private void openEditorForSite(@NonNull String targetHost) { } } + private void openEditorForSiteAndPost(@NonNull String targetHost, @NonNull String targetPostId) { + // Check if a site is available with given targetHost + SiteModel site = extractSiteModelFromTargetHost(targetHost); + String host = extractHostFromSite(site); + if (site == null || host == null || !StringUtils.equals(host, targetHost)) { + // Site not found, or host of site doesn't match the host in url + ToastUtils.showToast(getContext(), R.string.blog_not_found); + // Open a new post editor with current selected site + ActivityLauncher.openEditorInNewStack(getContext()); + return; + } + + Long remotePostId = parseAsLongOrNull(targetPostId); + + if (remotePostId == null) { + // No post id provided; open new post editor for given site + ActivityLauncher.openEditorForSiteInNewStack(getContext(), site); + return; + } + + // Check if post with given id is available for opening + PostModel post = mPostStore.getPostByRemotePostId(remotePostId, site); + + if (post == null) { + // Post not found + ToastUtils.showToast(getContext(), R.string.post_not_found); + // Open new post editor for given site + ActivityLauncher.openEditorForSiteInNewStack(getContext(), site); + return; + } + + // Open editor with post + ActivityLauncher.openEditorForPostInNewStack(getContext(), site, post.getId()); + } + private boolean shouldViewPost(String host) { return StringUtils.equals(host, DEEP_LINK_HOST_VIEWPOST); } @@ -208,9 +329,6 @@ private void handleAppBanner(@NonNull String host) { case DEEP_LINK_HOST_NOTIFICATIONS: ActivityLauncher.viewNotificationsInNewStack(getContext()); break; - case DEEP_LINK_HOST_POST: - ActivityLauncher.openEditorInNewStack(getContext()); - break; case DEEP_LINK_HOST_STATS: long primarySiteId = mAccountStore.getAccount().getPrimarySiteId(); SiteModel siteModel = mSiteStore.getSiteBySiteId(primarySiteId); @@ -225,7 +343,6 @@ private void handleAppBanner(@NonNull String host) { private boolean isFromAppBanner(String host) { return (host != null && (host.equals(DEEP_LINK_HOST_NOTIFICATIONS) - || host.equals(DEEP_LINK_HOST_POST) || host.equals(DEEP_LINK_HOST_READ) || host.equals(DEEP_LINK_HOST_STATS))); } @@ -287,4 +404,16 @@ private boolean shouldShow(@NonNull Uri uri, @NonNull String path) { return StringUtils.equals(uri.getHost(), HOST_WORDPRESS_COM) && (!uri.getPathSegments().isEmpty() && StringUtils.equals(uri.getPathSegments().get(0), path)); } + + private Long parseAsLongOrNull(String longAsString) { + if (longAsString == null || longAsString.isEmpty()) { + return null; + } + + try { + return Long.valueOf(longAsString); + } catch (NumberFormatException nfe) { + return null; + } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java b/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java index 8d52aace11b4..f2ff069602dd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java @@ -54,7 +54,6 @@ public class FilteredRecyclerView extends RelativeLayout { private RecyclerView mRecyclerView; private TextView mEmptyView; - private View mCustomEmptyView; private Toolbar mToolbar; private AppBarLayout mAppBarLayout; private RecyclerView mSearchSuggestionsRecyclerView; @@ -68,6 +67,7 @@ public class FilteredRecyclerView extends RelativeLayout { private int mSpinnerDrawableRight; private AppLog.T mTAG; + private boolean mShowEmptyView; private boolean mToolbarDisableScrollGestures = false; @LayoutRes private int mSpinnerItemView = 0; @LayoutRes private int mSpinnerDropDownItemView = 0; @@ -137,8 +137,8 @@ public void setLogT(AppLog.T tag) { mTAG = tag; } - public void setCustomEmptyView(View v) { - mCustomEmptyView = v; + public void setCustomEmptyView() { + mShowEmptyView = true; } private void init(@NonNull Context context, @Nullable AttributeSet attrs) { @@ -310,7 +310,7 @@ public void updateEmptyView(EmptyViewMessageType emptyViewMessageType) { if (!hasAdapter() || mAdapter.getItemCount() == 0) { if (mFilterListener != null) { - if (mCustomEmptyView == null) { + if (mShowEmptyView) { String msg = mFilterListener.onShowEmptyViewMessage(emptyViewMessageType); if (msg == null) { msg = getContext().getString(R.string.empty_list_default); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java index 9f36c38233a0..99a2c243b5f6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java @@ -62,7 +62,6 @@ import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.CrashLoggingUtils; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.SelfSignedSSLUtils; import org.wordpress.android.util.StringUtils; @@ -666,10 +665,6 @@ public void saveCredentialsInSmartLock(@Nullable final String username, @Nullabl // log some data to help us debug https://github.com/wordpress-mobile/WordPress-Android/issues/7182 final String loginModeStr = "LoginMode: " + (getLoginMode() != null ? getLoginMode().name() : "null"); AppLog.w(AppLog.T.NUX, "Internal inconsistency error! mSmartLockHelper found null!" + loginModeStr); - CrashLoggingUtils.logException( - new RuntimeException("Internal inconsistency error! mSmartLockHelper found null!"), - AppLog.T.NUX, - loginModeStr); // bail return; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.java deleted file mode 100644 index a22f834ce831..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.java +++ /dev/null @@ -1,500 +0,0 @@ -package org.wordpress.android.ui.main; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ScrollView; -import android.widget.TextView; - -import androidx.annotation.Nullable; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.yalantis.ucrop.UCrop; -import com.yalantis.ucrop.UCropActivity; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.jetbrains.annotations.NotNull; -import org.wordpress.android.R; -import org.wordpress.android.WordPress; -import org.wordpress.android.analytics.AnalyticsTracker; -import org.wordpress.android.fluxc.Dispatcher; -import org.wordpress.android.fluxc.model.AccountModel; -import org.wordpress.android.fluxc.model.SiteModel; -import org.wordpress.android.fluxc.store.AccountStore; -import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged; -import org.wordpress.android.fluxc.store.PostStore; -import org.wordpress.android.fluxc.store.SiteStore; -import org.wordpress.android.networking.GravatarApi; -import org.wordpress.android.ui.ActivityLauncher; -import org.wordpress.android.ui.RequestCodes; -import org.wordpress.android.ui.accounts.HelpActivity.Origin; -import org.wordpress.android.ui.main.utils.MeGravatarLoader; -import org.wordpress.android.ui.media.MediaBrowserType; -import org.wordpress.android.ui.photopicker.PhotoPickerActivity; -import org.wordpress.android.ui.photopicker.PhotoPickerActivity.PhotoPickerMediaSource; -import org.wordpress.android.ui.prefs.AppPrefsWrapper; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.FluxCUtils; -import org.wordpress.android.util.MediaUtils; -import org.wordpress.android.util.ToastUtils; -import org.wordpress.android.util.ToastUtils.Duration; -import org.wordpress.android.util.WPMediaUtils; -import org.wordpress.android.util.image.ImageManager; -import org.wordpress.android.util.image.ImageManager.RequestListener; -import org.wordpress.android.util.image.ImageType; - -import java.io.File; -import java.lang.ref.WeakReference; - -import javax.inject.Inject; - -public class MeFragment extends Fragment implements WPMainActivity.OnScrollToTopListener { - private static final String IS_DISCONNECTING = "IS_DISCONNECTING"; - private static final String IS_UPDATING_GRAVATAR = "IS_UPDATING_GRAVATAR"; - - private ViewGroup mAvatarCard; - private View mProgressBar; - private ImageView mAvatarImageView; - private TextView mDisplayNameTextView; - private TextView mUsernameTextView; - private TextView mLoginLogoutTextView; - private View mMyProfileView; - private View mAccountSettingsView; - private ProgressDialog mDisconnectProgressDialog; - private ScrollView mScrollView; - - @Nullable - private Toolbar mToolbar = null; - private String mToolbarTitle; - - private boolean mIsUpdatingGravatar; - - @Inject Dispatcher mDispatcher; - @Inject AccountStore mAccountStore; - @Inject SiteStore mSiteStore; - @Inject ImageManager mImageManager; - @Inject AppPrefsWrapper mAppPrefsWrapper; - @Inject PostStore mPostStore; - @Inject MeGravatarLoader mMeGravatarLoader; - - public static MeFragment newInstance() { - return new MeFragment(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ((WordPress) getActivity().getApplication()).component().inject(this); - - if (savedInstanceState != null) { - mIsUpdatingGravatar = savedInstanceState.getBoolean(IS_UPDATING_GRAVATAR); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.me_fragment, container, false); - - mAvatarCard = rootView.findViewById(R.id.card_avatar); - ViewGroup avatarContainer = rootView.findViewById(R.id.avatar_container); - mAvatarImageView = rootView.findViewById(R.id.me_avatar); - mProgressBar = rootView.findViewById(R.id.avatar_progress); - mDisplayNameTextView = rootView.findViewById(R.id.me_display_name); - mUsernameTextView = rootView.findViewById(R.id.me_username); - mLoginLogoutTextView = rootView.findViewById(R.id.me_login_logout_text_view); - mMyProfileView = rootView.findViewById(R.id.row_my_profile); - mAccountSettingsView = rootView.findViewById(R.id.row_account_settings); - mScrollView = rootView.findViewById(R.id.scroll_view); - - OnClickListener showPickerListener = v -> { - AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_TAPPED); - showPhotoPickerForGravatar(); - }; - - avatarContainer.setOnClickListener(showPickerListener); - rootView.findViewById(R.id.change_photo).setOnClickListener(showPickerListener); - - mMyProfileView.setOnClickListener(v -> ActivityLauncher.viewMyProfile(getActivity())); - - mAccountSettingsView.setOnClickListener(v -> ActivityLauncher.viewAccountSettings(getActivity())); - - rootView.findViewById(R.id.row_app_settings).setOnClickListener( - v -> ActivityLauncher.viewAppSettingsForResult(getActivity())); - - rootView.findViewById(R.id.row_support).setOnClickListener( - v -> ActivityLauncher - .viewHelpAndSupport(getActivity(), Origin.ME_SCREEN_HELP, getSelectedSite(), null)); - - rootView.findViewById(R.id.row_logout).setOnClickListener(v -> { - if (mAccountStore.hasAccessToken()) { - signOutWordPressComWithConfirmation(); - } else { - ActivityLauncher.showSignInForResult(getActivity()); - } - }); - - if (savedInstanceState != null) { - if (savedInstanceState.getBoolean(IS_DISCONNECTING, false)) { - showDisconnectDialog(getActivity()); - } - - if (savedInstanceState.getBoolean(IS_UPDATING_GRAVATAR, false)) { - showGravatarProgressBar(true); - } - } - - mToolbar = rootView.findViewById(R.id.toolbar_main); - mToolbar.setTitle(mToolbarTitle); - - return rootView; - } - - @Override - public void onSaveInstanceState(@NotNull Bundle outState) { - if (mDisconnectProgressDialog != null) { - outState.putBoolean(IS_DISCONNECTING, true); - } - outState.putBoolean(IS_UPDATING_GRAVATAR, mIsUpdatingGravatar); - - super.onSaveInstanceState(outState); - } - - @Override - public void onScrollToTop() { - if (isAdded()) { - mScrollView.smoothScrollTo(0, 0); - } - } - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - mDispatcher.register(this); - } - - @Override - public void onStop() { - mDispatcher.unregister(this); - EventBus.getDefault().unregister(this); - super.onStop(); - } - - @Override - public void onResume() { - super.onResume(); - refreshAccountDetails(); - } - - @Override - public void onDestroy() { - if (mDisconnectProgressDialog != null) { - mDisconnectProgressDialog.dismiss(); - mDisconnectProgressDialog = null; - } - super.onDestroy(); - } - - private void refreshAccountDetails() { - if (!FluxCUtils.isSignedInWPComOrHasWPOrgSite(mAccountStore, mSiteStore)) { - return; - } - // we only want to show user details for WordPress.com users - if (mAccountStore.hasAccessToken()) { - AccountModel defaultAccount = mAccountStore.getAccount(); - - mDisplayNameTextView.setVisibility(View.VISIBLE); - mUsernameTextView.setVisibility(View.VISIBLE); - mAvatarCard.setVisibility(View.VISIBLE); - mMyProfileView.setVisibility(View.VISIBLE); - - loadAvatar(null); - - mUsernameTextView.setText(getString(R.string.at_username, defaultAccount.getUserName())); - mLoginLogoutTextView.setText(R.string.me_disconnect_from_wordpress_com); - - String displayName = defaultAccount.getDisplayName(); - if (!TextUtils.isEmpty(displayName)) { - mDisplayNameTextView.setText(displayName); - } else { - mDisplayNameTextView.setText(defaultAccount.getUserName()); - } - } else { - mDisplayNameTextView.setVisibility(View.GONE); - mUsernameTextView.setVisibility(View.GONE); - mAvatarCard.setVisibility(View.GONE); - mProgressBar.setVisibility(View.GONE); - mMyProfileView.setVisibility(View.GONE); - mAccountSettingsView.setVisibility(View.GONE); - mLoginLogoutTextView.setText(R.string.me_connect_to_wordpress_com); - } - } - - private void showGravatarProgressBar(boolean isUpdating) { - mProgressBar.setVisibility(isUpdating ? View.VISIBLE : View.GONE); - mIsUpdatingGravatar = isUpdating; - } - - private void loadAvatar(String injectFilePath) { - final boolean newAvatarUploaded = injectFilePath != null && !injectFilePath.isEmpty(); - final String avatarUrl = mMeGravatarLoader.constructGravatarUrl(mAccountStore.getAccount().getAvatarUrl()); - - mMeGravatarLoader.load( - newAvatarUploaded, - avatarUrl, - injectFilePath, - mAvatarImageView, - ImageType.AVATAR_WITHOUT_BACKGROUND, - new RequestListener() { - @Override - public void onLoadFailed(@Nullable Exception e, @Nullable Object model) { - final String appLogMessage = "onLoadFailed while loading Gravatar image!"; - if (e == null) { - AppLog.e(T.MAIN, appLogMessage + " e == null"); - } else { - AppLog.e(T.MAIN, appLogMessage, e); - } - - // For some reason, the Activity can be null so, guard for it. See #8590. - if (getActivity() != null) { - ToastUtils.showToast(getActivity(), R.string.error_refreshing_gravatar, - ToastUtils.Duration.SHORT); - } - } - - @Override - public void onResourceReady(@NotNull Drawable resource, @Nullable Object model) { - if (newAvatarUploaded && resource instanceof BitmapDrawable) { - Bitmap bitmap = ((BitmapDrawable) resource).getBitmap(); - // create a copy since the original bitmap may by automatically recycled - bitmap = bitmap.copy(bitmap.getConfig(), true); - WordPress.getBitmapCache().put( - avatarUrl, - bitmap - ); - } - } - }); - } - - private void signOutWordPressComWithConfirmation() { - // if there are local changes we need to let the user know they'll be lost if they logout, otherwise - // we use a simpler (less scary!) confirmation - String message; - if (mPostStore.getNumLocalChanges() > 0) { - message = getString(R.string.sign_out_wpcom_confirm_with_changes); - } else { - message = getString(R.string.sign_out_wpcom_confirm_with_no_changes); - } - - new MaterialAlertDialogBuilder(getActivity()) - .setMessage(message) - .setPositiveButton(R.string.signout, (dialog, whichButton) -> signOutWordPressCom()) - .setNegativeButton(R.string.cancel, null) - .setCancelable(true) - .create().show(); - } - - private void signOutWordPressCom() { - // note that signing out sends a CoreEvents.UserSignedOutWordPressCom EventBus event, - // which will cause the main activity to recreate this fragment - (new SignOutWordPressComAsync(getActivity())).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private void showDisconnectDialog(Context context) { - mDisconnectProgressDialog = ProgressDialog.show(context, null, context.getText(R.string.signing_out), false); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - // If the fragment is not attached to the activity, we can't start the crop activity or upload the - // cropped image. - if (!isAdded()) { - return; - } - - switch (requestCode) { - case RequestCodes.PHOTO_PICKER: - if (resultCode == Activity.RESULT_OK && data != null) { - String[] mediaUriStringsArray = data.getStringArrayExtra(PhotoPickerActivity.EXTRA_MEDIA_URIS); - if (mediaUriStringsArray == null || mediaUriStringsArray.length == 0) { - AppLog.e(AppLog.T.UTILS, "Can't resolve picked or captured image"); - return; - } - PhotoPickerMediaSource source = PhotoPickerMediaSource.fromString( - data.getStringExtra(PhotoPickerActivity.EXTRA_MEDIA_SOURCE)); - AnalyticsTracker.Stat stat = - source == PhotoPickerMediaSource.ANDROID_CAMERA - ? AnalyticsTracker.Stat.ME_GRAVATAR_SHOT_NEW - : AnalyticsTracker.Stat.ME_GRAVATAR_GALLERY_PICKED; - AnalyticsTracker.track(stat); - Uri imageUri = Uri.parse(mediaUriStringsArray[0]); - if (imageUri != null) { - boolean didGoWell = WPMediaUtils.fetchMediaAndDoNext(getActivity(), imageUri, - this::startCropActivity); - - if (!didGoWell) { - AppLog.e(AppLog.T.UTILS, "Can't download picked or captured image"); - } - } - } - break; - case UCrop.REQUEST_CROP: - AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_CROPPED); - - if (resultCode == Activity.RESULT_OK) { - WPMediaUtils.fetchMediaAndDoNext(getActivity(), UCrop.getOutput(data), - uri -> startGravatarUpload( - MediaUtils.getRealPathFromURI(getActivity(), uri))); - } else if (resultCode == UCrop.RESULT_ERROR) { - AppLog.e(AppLog.T.MAIN, "Image cropping failed!", UCrop.getError(data)); - ToastUtils.showToast(getActivity(), R.string.error_cropping_image, Duration.SHORT); - } - break; - } - } - - private void showPhotoPickerForGravatar() { - ActivityLauncher.showPhotoPickerForResult(this, MediaBrowserType.GRAVATAR_IMAGE_PICKER, null, null); - } - - private void startCropActivity(Uri uri) { - final Context context = getActivity(); - - if (context == null) { - return; - } - - UCrop.Options options = new UCrop.Options(); - options.setShowCropGrid(false); - options.setStatusBarColor(ContextCompat.getColor(context, R.color.status_bar)); - options.setToolbarColor(ContextCompat.getColor(context, R.color.primary)); - options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.NONE, UCropActivity.NONE); - options.setHideBottomControls(true); - - UCrop.of(uri, Uri.fromFile(new File(context.getCacheDir(), "cropped_for_gravatar.jpg"))) - .withAspectRatio(1, 1) - .withOptions(options) - .start(getActivity(), this); - } - - private void startGravatarUpload(final String filePath) { - if (TextUtils.isEmpty(filePath)) { - ToastUtils.showToast(getActivity(), R.string.error_locating_image, ToastUtils.Duration.SHORT); - return; - } - - File file = new File(filePath); - if (!file.exists()) { - ToastUtils.showToast(getActivity(), R.string.error_locating_image, ToastUtils.Duration.SHORT); - return; - } - - showGravatarProgressBar(true); - - GravatarApi.uploadGravatar(file, mAccountStore.getAccount().getEmail(), mAccountStore.getAccessToken(), - new GravatarApi.GravatarUploadListener() { - @Override - public void onSuccess() { - EventBus.getDefault().post(new GravatarUploadFinished(filePath, true)); - } - - @Override - public void onError() { - EventBus.getDefault().post(new GravatarUploadFinished(filePath, false)); - } - }); - } - - public static class GravatarUploadFinished { - public final String filePath; - public final boolean success; - - GravatarUploadFinished(String filePath, boolean success) { - this.filePath = filePath; - this.success = success; - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(GravatarUploadFinished event) { - showGravatarProgressBar(false); - if (event.success) { - AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_UPLOADED); - loadAvatar(event.filePath); - } else { - ToastUtils.showToast(getActivity(), R.string.error_updating_gravatar, ToastUtils.Duration.SHORT); - } - } - - private class SignOutWordPressComAsync extends AsyncTask { - WeakReference mWeakContext; - - SignOutWordPressComAsync(Context context) { - mWeakContext = new WeakReference(context); - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - Context context = mWeakContext.get(); - if (context != null) { - showDisconnectDialog(context); - } - } - - @Override - protected Void doInBackground(Void... params) { - Context context = mWeakContext.get(); - if (context != null) { - ((WordPress) getActivity().getApplication()).wordPressComSignOut(); - } - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - if (mDisconnectProgressDialog != null && mDisconnectProgressDialog.isShowing()) { - mDisconnectProgressDialog.dismiss(); - } - mDisconnectProgressDialog = null; - } - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onAccountChanged(OnAccountChanged event) { - refreshAccountDetails(); - } - - private @Nullable SiteModel getSelectedSite() { - if (getActivity() instanceof WPMainActivity) { - WPMainActivity mainActivity = (WPMainActivity) getActivity(); - return mainActivity.getSelectedSite(); - } - return null; - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt new file mode 100644 index 000000000000..dc4cf88ac6c8 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt @@ -0,0 +1,457 @@ +package org.wordpress.android.ui.main + +import android.app.Activity +import android.app.ProgressDialog +import android.content.Intent +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnClickListener +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.yalantis.ucrop.UCrop +import com.yalantis.ucrop.UCrop.Options +import com.yalantis.ucrop.UCropActivity +import kotlinx.android.synthetic.main.me_fragment.* +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat.ME_GRAVATAR_CROPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.ME_GRAVATAR_GALLERY_PICKED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.ME_GRAVATAR_SHOT_NEW +import org.wordpress.android.analytics.AnalyticsTracker.Stat.ME_GRAVATAR_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.ME_GRAVATAR_UPLOADED +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged +import org.wordpress.android.fluxc.store.PostStore +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.networking.GravatarApi +import org.wordpress.android.networking.GravatarApi.GravatarUploadListener +import org.wordpress.android.ui.ActivityLauncher +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.ui.accounts.HelpActivity.Origin.ME_SCREEN_HELP +import org.wordpress.android.ui.main.WPMainActivity.OnScrollToTopListener +import org.wordpress.android.ui.main.utils.MeGravatarLoader +import org.wordpress.android.ui.media.MediaBrowserType.GRAVATAR_IMAGE_PICKER +import org.wordpress.android.ui.photopicker.PhotoPickerActivity +import org.wordpress.android.ui.photopicker.PhotoPickerActivity.PhotoPickerMediaSource +import org.wordpress.android.ui.photopicker.PhotoPickerActivity.PhotoPickerMediaSource.ANDROID_CAMERA +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T.MAIN +import org.wordpress.android.util.AppLog.T.UTILS +import org.wordpress.android.util.FluxCUtils +import org.wordpress.android.util.MediaUtils +import org.wordpress.android.util.ToastUtils +import org.wordpress.android.util.ToastUtils.Duration.SHORT +import org.wordpress.android.util.WPMediaUtils +import org.wordpress.android.util.image.ImageManager.RequestListener +import org.wordpress.android.util.image.ImageType.AVATAR_WITHOUT_BACKGROUND +import java.io.File +import javax.inject.Inject + +class MeFragment : Fragment(), OnScrollToTopListener { + private var disconnectProgressDialog: ProgressDialog? = null + private var isUpdatingGravatar = false + + @Inject lateinit var dispatcher: Dispatcher + @Inject lateinit var accountStore: AccountStore + @Inject lateinit var siteStore: SiteStore + @Inject lateinit var postStore: PostStore + @Inject lateinit var meGravatarLoader: MeGravatarLoader + @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var viewModel: MeViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + if (savedInstanceState != null) { + isUpdatingGravatar = savedInstanceState.getBoolean(IS_UPDATING_GRAVATAR) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.me_fragment, container, false) as ViewGroup + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val showPickerListener = OnClickListener { + AnalyticsTracker.track(ME_GRAVATAR_TAPPED) + showPhotoPickerForGravatar() + } + avatar_container.setOnClickListener(showPickerListener) + change_photo.setOnClickListener(showPickerListener) + row_my_profile.setOnClickListener { + ActivityLauncher.viewMyProfile( + activity + ) + } + row_account_settings.setOnClickListener { + ActivityLauncher.viewAccountSettings( + activity + ) + } + row_app_settings.setOnClickListener { + ActivityLauncher.viewAppSettingsForResult( + activity + ) + } + row_support.setOnClickListener { + ActivityLauncher + .viewHelpAndSupport( + requireContext(), + ME_SCREEN_HELP, + selectedSite, + null + ) + } + row_logout.setOnClickListener { + if (accountStore.hasAccessToken()) { + signOutWordPressComWithConfirmation() + } else { + ActivityLauncher.showSignInForResult(activity) + } + } + if (savedInstanceState != null) { + if (savedInstanceState.getBoolean(IS_DISCONNECTING, false)) { + viewModel.openDisconnectDialog() + } + if (savedInstanceState.getBoolean(IS_UPDATING_GRAVATAR, false)) { + showGravatarProgressBar(true) + } + } + + viewModel = ViewModelProviders.of(this, viewModelFactory).get(MeViewModel::class.java) + viewModel.showDisconnectDialog.observe(viewLifecycleOwner, Observer { + it.applyIfNotHandled { + when (this) { + true -> showDisconnectDialog() + false -> hideDisconnectDialog() + } + } + }) + } + + override fun onSaveInstanceState(outState: Bundle) { + if (disconnectProgressDialog != null) { + outState.putBoolean(IS_DISCONNECTING, true) + } + outState.putBoolean(IS_UPDATING_GRAVATAR, isUpdatingGravatar) + super.onSaveInstanceState(outState) + } + + override fun onScrollToTop() { + if (isAdded) { + scroll_view.smoothScrollTo(0, 0) + } + } + + override fun onStart() { + super.onStart() + EventBus.getDefault().register(this) + dispatcher.register(this) + } + + override fun onStop() { + dispatcher.unregister(this) + EventBus.getDefault().unregister(this) + super.onStop() + } + + override fun onResume() { + super.onResume() + refreshAccountDetails() + } + + override fun onDestroy() { + disconnectProgressDialog?.dismiss() + disconnectProgressDialog = null + super.onDestroy() + } + + private fun refreshAccountDetails() { + if (!FluxCUtils.isSignedInWPComOrHasWPOrgSite(accountStore, siteStore)) { + return + } + // we only want to show user details for WordPress.com users + if (accountStore.hasAccessToken()) { + val defaultAccount = accountStore.account + me_display_name.visibility = View.VISIBLE + me_username.visibility = View.VISIBLE + card_avatar.visibility = View.VISIBLE + row_my_profile.visibility = View.VISIBLE + loadAvatar(null) + me_username.text = getString(R.string.at_username, defaultAccount.userName) + me_login_logout_text_view.setText(R.string.me_disconnect_from_wordpress_com) + val displayName = defaultAccount.displayName + if (!TextUtils.isEmpty(displayName)) { + me_display_name.text = displayName + } else { + me_display_name.text = defaultAccount.userName + } + } else { + me_display_name.visibility = View.GONE + me_username.visibility = View.GONE + card_avatar.visibility = View.GONE + avatar_progress.visibility = View.GONE + row_my_profile.visibility = View.GONE + row_account_settings.visibility = View.GONE + me_login_logout_text_view.setText(R.string.me_connect_to_wordpress_com) + } + } + + private fun showGravatarProgressBar(isUpdating: Boolean) { + avatar_progress.visibility = if (isUpdating) View.VISIBLE else View.GONE + isUpdatingGravatar = isUpdating + } + + private fun loadAvatar(injectFilePath: String?) { + val newAvatarUploaded = injectFilePath != null && injectFilePath.isNotEmpty() + val avatarUrl = meGravatarLoader.constructGravatarUrl(accountStore.account.avatarUrl) + meGravatarLoader.load( + newAvatarUploaded, + avatarUrl, + injectFilePath, + me_avatar, + AVATAR_WITHOUT_BACKGROUND, + object : RequestListener { + override fun onLoadFailed(e: Exception?, model: Any?) { + val appLogMessage = "onLoadFailed while loading Gravatar image!" + if (e == null) { + AppLog.e( + MAIN, + "$appLogMessage e == null" + ) + } else { + AppLog.e( + MAIN, + appLogMessage, + e + ) + } + + // For some reason, the Activity can be null so, guard for it. See #8590. + if (activity != null) { + ToastUtils.showToast( + activity, R.string.error_refreshing_gravatar, + SHORT + ) + } + } + + override fun onResourceReady( + resource: Drawable, + model: Any? + ) { + if (newAvatarUploaded && resource is BitmapDrawable) { + var bitmap = resource.bitmap + // create a copy since the original bitmap may by automatically recycled + bitmap = bitmap.copy(bitmap.config, true) + WordPress.getBitmapCache().put( + avatarUrl, + bitmap + ) + } + } + }) + } + + private fun signOutWordPressComWithConfirmation() { + // if there are local changes we need to let the user know they'll be lost if they logout, otherwise + // we use a simpler (less scary!) confirmation + val message: String = if (postStore.numLocalChanges > 0) { + getString(R.string.sign_out_wpcom_confirm_with_changes) + } else { + getString(R.string.sign_out_wpcom_confirm_with_no_changes) + } + MaterialAlertDialogBuilder(activity) + .setMessage(message) + .setPositiveButton( + R.string.signout + ) { _, _ -> signOutWordPressCom() } + .setNegativeButton(R.string.cancel, null) + .setCancelable(true) + .create().show() + } + + private fun signOutWordPressCom() { + viewModel.signOutWordPress(requireActivity().application as WordPress) + } + + private fun showDisconnectDialog() { + disconnectProgressDialog = ProgressDialog.show( + requireContext(), + null, + requireContext().getText(R.string.signing_out), + false + ) + } + + private fun hideDisconnectDialog() { + if (disconnectProgressDialog?.isShowing == true) { + disconnectProgressDialog?.dismiss() + } + disconnectProgressDialog = null + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + // If the fragment is not attached to the activity, we can't start the crop activity or upload the + // cropped image. + if (!isAdded) { + return + } + when (requestCode) { + RequestCodes.PHOTO_PICKER -> if (resultCode == Activity.RESULT_OK && data != null) { + val mediaUriStringsArray = data.getStringArrayExtra(PhotoPickerActivity.EXTRA_MEDIA_URIS) + if (mediaUriStringsArray == null || mediaUriStringsArray.size == 0) { + AppLog.e( + UTILS, + "Can't resolve picked or captured image" + ) + return + } + val source = PhotoPickerMediaSource.fromString( + data.getStringExtra(PhotoPickerActivity.EXTRA_MEDIA_SOURCE) + ) + val stat = if (source == ANDROID_CAMERA) ME_GRAVATAR_SHOT_NEW else ME_GRAVATAR_GALLERY_PICKED + AnalyticsTracker.track(stat) + val imageUri = Uri.parse(mediaUriStringsArray[0]) + if (imageUri != null) { + val didGoWell = WPMediaUtils.fetchMediaAndDoNext( + activity, + imageUri + ) { uri: Uri -> startCropActivity(uri) } + if (!didGoWell) { + AppLog.e( + UTILS, + "Can't download picked or captured image" + ) + } + } + } + UCrop.REQUEST_CROP -> { + AnalyticsTracker.track(ME_GRAVATAR_CROPPED) + if (resultCode == Activity.RESULT_OK) { + WPMediaUtils.fetchMediaAndDoNext( + activity, UCrop.getOutput(data!!) + ) { uri: Uri? -> + startGravatarUpload( + MediaUtils.getRealPathFromURI(activity, uri) + ) + } + } else if (resultCode == UCrop.RESULT_ERROR) { + AppLog.e( + MAIN, + "Image cropping failed!", + UCrop.getError(data!!) + ) + ToastUtils.showToast( + activity, + R.string.error_cropping_image, + SHORT + ) + } + } + } + } + + private fun showPhotoPickerForGravatar() { + ActivityLauncher.showPhotoPickerForResult(this, GRAVATAR_IMAGE_PICKER, null, null) + } + + private fun startCropActivity(uri: Uri) { + val context = activity ?: return + val options = Options() + options.setShowCropGrid(false) + options.setStatusBarColor(ContextCompat.getColor(context, R.color.status_bar)) + options.setToolbarColor(ContextCompat.getColor(context, R.color.primary)) + options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.NONE, UCropActivity.NONE) + options.setHideBottomControls(true) + UCrop.of(uri, Uri.fromFile(File(context.cacheDir, "cropped_for_gravatar.jpg"))) + .withAspectRatio(1f, 1f) + .withOptions(options) + .start(requireActivity(), this) + } + + private fun startGravatarUpload(filePath: String) { + if (TextUtils.isEmpty(filePath)) { + ToastUtils.showToast( + activity, + R.string.error_locating_image, + SHORT + ) + return + } + val file = File(filePath) + if (!file.exists()) { + ToastUtils.showToast( + activity, + R.string.error_locating_image, + SHORT + ) + return + } + showGravatarProgressBar(true) + GravatarApi.uploadGravatar(file, accountStore.account.email, accountStore.accessToken, + object : GravatarUploadListener { + override fun onSuccess() { + EventBus.getDefault().post(GravatarUploadFinished(filePath, true)) + } + + override fun onError() { + EventBus.getDefault().post(GravatarUploadFinished(filePath, false)) + } + }) + } + + class GravatarUploadFinished internal constructor(val filePath: String, val success: Boolean) + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(event: GravatarUploadFinished) { + showGravatarProgressBar(false) + if (event.success) { + AnalyticsTracker.track(ME_GRAVATAR_UPLOADED) + loadAvatar(event.filePath) + } else { + ToastUtils.showToast( + activity, + R.string.error_updating_gravatar, + SHORT + ) + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onAccountChanged(event: OnAccountChanged?) { + refreshAccountDetails() + } + + private val selectedSite: SiteModel? + get() { + return (activity as? WPMainActivity)?.selectedSite + } + + companion object { + private const val IS_DISCONNECTING = "IS_DISCONNECTING" + private const val IS_UPDATING_GRAVATAR = "IS_UPDATING_GRAVATAR" + fun newInstance(): MeFragment { + return MeFragment() + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MeViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MeViewModel.kt new file mode 100644 index 000000000000..c7714015621e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MeViewModel.kt @@ -0,0 +1,37 @@ +package org.wordpress.android.ui.main + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.wordpress.android.WordPress +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.viewmodel.Event +import org.wordpress.android.viewmodel.ScopedViewModel +import javax.inject.Inject +import javax.inject.Named + +class MeViewModel +@Inject constructor( + @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, + @Named(BG_THREAD) val bgDispatcher: CoroutineDispatcher +) : ScopedViewModel(mainDispatcher) { + private val _showDisconnectDialog = MutableLiveData>() + val showDisconnectDialog: LiveData> = _showDisconnectDialog + + fun signOutWordPress(application: WordPress) { + launch { + _showDisconnectDialog.value = Event(true) + withContext(bgDispatcher) { + application.wordPressComSignOut() + } + _showDisconnectDialog.value = Event(false) + } + } + + fun openDisconnectDialog() { + _showDisconnectDialog.value = Event(true) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java deleted file mode 100644 index ff9f5da3533a..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.java +++ /dev/null @@ -1,1389 +0,0 @@ -package org.wordpress.android.ui.main; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.graphics.Paint; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.text.Spannable; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.ScrollView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.PopupMenu; -import androidx.appcompat.widget.Toolbar; -import androidx.appcompat.widget.TooltipCompat; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; - -import com.yalantis.ucrop.UCrop; -import com.yalantis.ucrop.UCropActivity; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.jetbrains.annotations.NotNull; -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.fluxc.Dispatcher; -import org.wordpress.android.fluxc.generated.SiteActionBuilder; -import org.wordpress.android.fluxc.model.MediaModel; -import org.wordpress.android.fluxc.model.SiteModel; -import org.wordpress.android.fluxc.store.AccountStore; -import org.wordpress.android.fluxc.store.MediaStore; -import org.wordpress.android.fluxc.store.QuickStartStore; -import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask; -import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType; -import org.wordpress.android.fluxc.store.SiteStore.OnPlansFetched; -import org.wordpress.android.login.LoginMode; -import org.wordpress.android.ui.ActionableEmptyView; -import org.wordpress.android.ui.ActivityLauncher; -import org.wordpress.android.ui.FullScreenDialogFragment; -import org.wordpress.android.ui.FullScreenDialogFragment.OnConfirmListener; -import org.wordpress.android.ui.FullScreenDialogFragment.OnDismissListener; -import org.wordpress.android.ui.RequestCodes; -import org.wordpress.android.ui.accounts.LoginActivity; -import org.wordpress.android.ui.comments.CommentsListFragment.CommentStatusCriteria; -import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose; -import org.wordpress.android.ui.domains.DomainRegistrationResultFragment; -import org.wordpress.android.ui.main.utils.MeGravatarLoader; -import org.wordpress.android.ui.media.MediaBrowserType; -import org.wordpress.android.ui.photopicker.PhotoPickerActivity; -import org.wordpress.android.ui.photopicker.PhotoPickerActivity.PhotoPickerMediaSource; -import org.wordpress.android.ui.plugins.PluginUtils; -import org.wordpress.android.ui.posts.BasicFragmentDialog; -import org.wordpress.android.ui.posts.PromoDialog; -import org.wordpress.android.ui.posts.PromoDialog.PromoDialogClickInterface; -import org.wordpress.android.ui.prefs.AppPrefs; -import org.wordpress.android.ui.prefs.SiteSettingsInterface; -import org.wordpress.android.ui.prefs.SiteSettingsInterface.SiteSettingsListener; -import org.wordpress.android.ui.quickstart.QuickStartEvent; -import org.wordpress.android.ui.quickstart.QuickStartFullScreenDialogFragment; -import org.wordpress.android.ui.quickstart.QuickStartMySitePrompts; -import org.wordpress.android.ui.quickstart.QuickStartNoticeDetails; -import org.wordpress.android.ui.themes.ThemeBrowserActivity; -import org.wordpress.android.ui.uploads.UploadService; -import org.wordpress.android.ui.uploads.UploadUtilsWrapper; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.DateTimeUtils; -import org.wordpress.android.util.DisplayUtils; -import org.wordpress.android.util.FluxCUtils; -import org.wordpress.android.util.MediaUtils; -import org.wordpress.android.util.PhotonUtils; -import org.wordpress.android.util.QuickStartUtils; -import org.wordpress.android.util.SiteUtils; -import org.wordpress.android.util.ToastUtils; -import org.wordpress.android.util.ToastUtils.Duration; -import org.wordpress.android.util.WPMediaUtils; -import org.wordpress.android.util.analytics.AnalyticsUtils; -import org.wordpress.android.util.image.ImageManager; -import org.wordpress.android.util.image.ImageType; -import org.wordpress.android.widgets.WPDialogSnackbar; -import org.wordpress.android.widgets.WPTextView; - -import java.io.File; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.TimeZone; - -import javax.inject.Inject; - -import static org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType.CUSTOMIZE; -import static org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType.GROW; -import static org.wordpress.android.ui.plans.PlanUtilsKt.isDomainCreditAvailable; -import static org.wordpress.android.ui.quickstart.QuickStartFullScreenDialogFragment.RESULT_TASK; -import static org.wordpress.android.util.DomainRegistrationUtilsKt.requestEmailValidation; - -public class MySiteFragment extends Fragment implements - SiteSettingsListener, - WPMainActivity.OnScrollToTopListener, - BasicFragmentDialog.BasicDialogPositiveClickInterface, - BasicFragmentDialog.BasicDialogNegativeClickInterface, - BasicFragmentDialog.BasicDialogOnDismissByOutsideTouchInterface, PromoDialogClickInterface, - OnConfirmListener, OnDismissListener { - public static final int HIDE_WP_ADMIN_YEAR = 2015; - public static final int HIDE_WP_ADMIN_MONTH = 9; - public static final int HIDE_WP_ADMIN_DAY = 7; - public static final String HIDE_WP_ADMIN_GMT_TIME_ZONE = "GMT"; - public static final String ARG_QUICK_START_TASK = "ARG_QUICK_START_TASK"; - public static final String TAG_ADD_SITE_ICON_DIALOG = "TAG_ADD_SITE_ICON_DIALOG"; - public static final String TAG_REMOVE_NEXT_STEPS_DIALOG = "TAG_REMOVE_NEXT_STEPS_DIALOG"; - public static final String TAG_CHANGE_SITE_ICON_DIALOG = "TAG_CHANGE_SITE_ICON_DIALOG"; - public static final String TAG_EDIT_SITE_ICON_PERMISSIONS_DIALOG = "TAG_EDIT_SITE_ICON_PERMISSIONS_DIALOG"; - public static final String TAG_QUICK_START_DIALOG = "TAG_QUICK_START_DIALOG"; - public static final String TAG_QUICK_START_MIGRATION_DIALOG = "TAG_QUICK_START_MIGRATION_DIALOG"; - public static final int AUTO_QUICK_START_SNACKBAR_DELAY_MS = 1000; - public static final String KEY_IS_DOMAIN_CREDIT_AVAILABLE = "KEY_IS_DOMAIN_CREDIT_AVAILABLE"; - public static final String KEY_DOMAIN_CREDIT_CHECKED = "KEY_DOMAIN_CREDIT_CHECKED"; - - private ImageView mBlavatarImageView; - private ImageView mAvatarImageView; - private ProgressBar mBlavatarProgressBar; - private WPTextView mBlogTitleTextView; - private WPTextView mBlogSubtitleTextView; - private WPTextView mLookAndFeelHeader; - private LinearLayout mThemesContainer; - private LinearLayout mPeopleView; - private LinearLayout mPageView; - private View mQuickActionPageButtonContainer; - private LinearLayout mQuickActionButtonsContainer; - private LinearLayout mPlanContainer; - private LinearLayout mPluginsContainer; - private LinearLayout mActivityLogContainer; - private View mQuickStartContainer; - private WPTextView mConfigurationHeader; - private View mSettingsView; - private LinearLayout mAdminView; - private ActionableEmptyView mActionableEmptyView; - private ScrollView mScrollView; - private WPTextView mCurrentPlanNameTextView; - private View mSharingView; - private SiteSettingsInterface mSiteSettings; - private QuickStartMySitePrompts mActiveTutorialPrompt; - private ImageView mQuickStartCustomizeIcon; - private TextView mQuickStartCustomizeSubtitle; - private TextView mQuickStartCustomizeTitle; - private View mQuickStartCustomizeView; - private ImageView mQuickStartGrowIcon; - private TextView mQuickStartGrowSubtitle; - private TextView mQuickStartGrowTitle; - private View mQuickStartGrowView; - private View mQuickStartMenuButton; - private View mDomainRegistrationCta; - - private Handler mQuickStartSnackBarHandler = new Handler(); - - @Nullable - private Toolbar mToolbar = null; - - private int mBlavatarSz; - private boolean mIsDomainCreditAvailable = false; - private boolean mIsDomainCreditChecked = false; - - @Inject AccountStore mAccountStore; - @Inject Dispatcher mDispatcher; - @Inject MediaStore mMediaStore; - @Inject QuickStartStore mQuickStartStore; - @Inject ImageManager mImageManager; - @Inject UploadUtilsWrapper mUploadUtilsWrapper; - @Inject MeGravatarLoader mMeGravatarLoader; - - public static MySiteFragment newInstance() { - return new MySiteFragment(); - } - - public @Nullable SiteModel getSelectedSite() { - if (getActivity() instanceof WPMainActivity) { - WPMainActivity mainActivity = (WPMainActivity) getActivity(); - return mainActivity.getSelectedSite(); - } - return null; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ((WordPress) requireActivity().getApplication()).component().inject(this); - - if (savedInstanceState != null) { - mActiveTutorialPrompt = - (QuickStartMySitePrompts) savedInstanceState.getSerializable(QuickStartMySitePrompts.KEY); - mIsDomainCreditAvailable = savedInstanceState.getBoolean(KEY_IS_DOMAIN_CREDIT_AVAILABLE, false); - mIsDomainCreditChecked = savedInstanceState.getBoolean(KEY_DOMAIN_CREDIT_CHECKED, false); - } - } - - private void refreshMeGravatar() { - String avatarUrl = mMeGravatarLoader.constructGravatarUrl(mAccountStore.getAccount().getAvatarUrl()); - - mMeGravatarLoader.load( - false, - avatarUrl, - null, - mAvatarImageView, - ImageType.USER, - null - ); - } - - @Override - public void onResume() { - super.onResume(); - - updateSiteSettingsIfNecessary(); - - // Site details may have changed (e.g. via Settings and returning to this Fragment) so update the UI - refreshSelectedSiteDetails(getSelectedSite()); - - refreshMeGravatar(); - - SiteModel site = getSelectedSite(); - if (site != null) { - boolean isNotAdmin = !site.getHasCapabilityManageOptions(); - boolean isSelfHostedWithoutJetpack = !SiteUtils.isAccessedViaWPComRest(site) && !site.isJetpackConnected(); - if (isNotAdmin || isSelfHostedWithoutJetpack) { - mActivityLogContainer.setVisibility(View.GONE); - } else { - mActivityLogContainer.setVisibility(View.VISIBLE); - } - } - - updateQuickStartContainer(); - - if (!AppPrefs.hasQuickStartMigrationDialogShown() && QuickStartUtils.isQuickStartInProgress(mQuickStartStore)) { - showQuickStartDialogMigration(); - } - - showQuickStartNoticeIfNecessary(); - } - - private void showQuickStartNoticeIfNecessary() { - if (!QuickStartUtils.isQuickStartInProgress(mQuickStartStore) || !AppPrefs.isQuickStartNoticeRequired()) { - return; - } - - final QuickStartTask taskToPrompt = QuickStartUtils.getNextUncompletedQuickStartTask(mQuickStartStore, - AppPrefs.getSelectedSite(), CUSTOMIZE); // CUSTOMIZE is default type - - if (taskToPrompt != null) { - mQuickStartSnackBarHandler.removeCallbacksAndMessages(null); - mQuickStartSnackBarHandler.postDelayed(() -> { - if (!isAdded() || getView() == null || !(getActivity() instanceof WPMainActivity)) { - return; - } - - QuickStartNoticeDetails noticeDetails = QuickStartNoticeDetails.getNoticeForTask(taskToPrompt); - if (noticeDetails == null) { - return; - } - - String noticeTitle = getString(noticeDetails.getTitleResId()); - String noticeMessage = getString(noticeDetails.getMessageResId()); - - WPDialogSnackbar quickStartNoticeSnackBar = - WPDialogSnackbar.make( - requireActivity().findViewById(R.id.coordinator), - noticeMessage, - getResources().getInteger(R.integer.quick_start_snackbar_duration_ms)); - - quickStartNoticeSnackBar.setTitle(noticeTitle); - - quickStartNoticeSnackBar.setPositiveButton( - getString(R.string.quick_start_button_positive), v -> { - AnalyticsTracker.track(Stat.QUICK_START_TASK_DIALOG_POSITIVE_TAPPED); - mActiveTutorialPrompt = - QuickStartMySitePrompts.getPromptDetailsForTask(taskToPrompt); - showActiveQuickStartTutorial(); - }); - - quickStartNoticeSnackBar - .setNegativeButton(getString(R.string.quick_start_button_negative), - v -> AnalyticsTracker.track(Stat.QUICK_START_TASK_DIALOG_NEGATIVE_TAPPED)); - - ((WPMainActivity) requireActivity()).showQuickStartSnackBar(quickStartNoticeSnackBar); - - AnalyticsTracker.track(Stat.QUICK_START_TASK_DIALOG_VIEWED); - AppPrefs.setQuickStartNoticeRequired(false); - }, AUTO_QUICK_START_SNACKBAR_DELAY_MS); - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putSerializable(QuickStartMySitePrompts.KEY, mActiveTutorialPrompt); - outState.putBoolean(KEY_IS_DOMAIN_CREDIT_AVAILABLE, mIsDomainCreditAvailable); - outState.putBoolean(KEY_DOMAIN_CREDIT_CHECKED, mIsDomainCreditChecked); - } - - private void updateSiteSettingsIfNecessary() { - SiteModel selectedSite = getSelectedSite(); - if (selectedSite == null) { - // If the selected site is null, we can't update its site settings - return; - } - if (mSiteSettings != null && mSiteSettings.getLocalSiteId() != selectedSite.getId()) { - // The site has changed, we can't use the previous site settings, force a refresh - mSiteSettings = null; - } - if (mSiteSettings == null) { - mSiteSettings = SiteSettingsInterface.getInterface(getActivity(), getSelectedSite(), this); - if (mSiteSettings != null) { - mSiteSettings.init(true); - } - } - } - - @Override - public void onPause() { - super.onPause(); - clearActiveQuickStart(); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.my_site_fragment, container, false); - - mBlavatarSz = getResources().getDimensionPixelSize(R.dimen.blavatar_sz_small); - - mBlavatarImageView = rootView.findViewById(R.id.my_site_blavatar); - mBlavatarProgressBar = rootView.findViewById(R.id.my_site_icon_progress); - mBlogTitleTextView = rootView.findViewById(R.id.my_site_title_label); - mBlogSubtitleTextView = rootView.findViewById(R.id.my_site_subtitle_label); - mLookAndFeelHeader = rootView.findViewById(R.id.my_site_look_and_feel_header); - mThemesContainer = rootView.findViewById(R.id.row_themes); - mPeopleView = rootView.findViewById(R.id.row_people); - mPlanContainer = rootView.findViewById(R.id.row_plan); - mPluginsContainer = rootView.findViewById(R.id.row_plugins); - mActivityLogContainer = rootView.findViewById(R.id.row_activity_log); - mConfigurationHeader = rootView.findViewById(R.id.my_site_configuration_header); - mSettingsView = rootView.findViewById(R.id.row_settings); - mSharingView = rootView.findViewById(R.id.row_sharing); - mAdminView = rootView.findViewById(R.id.row_admin); - mScrollView = rootView.findViewById(R.id.scroll_view); - mActionableEmptyView = rootView.findViewById(R.id.actionable_empty_view); - mCurrentPlanNameTextView = rootView.findViewById(R.id.my_site_current_plan_text_view); - mPageView = rootView.findViewById(R.id.row_pages); - mQuickActionPageButtonContainer = rootView.findViewById(R.id.quick_action_pages_container); - mQuickActionButtonsContainer = rootView.findViewById(R.id.quick_action_buttons_container); - mQuickStartContainer = rootView.findViewById(R.id.quick_start); - mQuickStartCustomizeView = rootView.findViewById(R.id.quick_start_customize); - mQuickStartCustomizeIcon = rootView.findViewById(R.id.quick_start_customize_icon); - mQuickStartCustomizeSubtitle = rootView.findViewById(R.id.quick_start_customize_subtitle); - mQuickStartCustomizeTitle = rootView.findViewById(R.id.quick_start_customize_title); - mQuickStartGrowView = rootView.findViewById(R.id.quick_start_grow); - mQuickStartGrowIcon = rootView.findViewById(R.id.quick_start_grow_icon); - mQuickStartGrowSubtitle = rootView.findViewById(R.id.quick_start_grow_subtitle); - mQuickStartGrowTitle = rootView.findViewById(R.id.quick_start_grow_title); - mQuickStartMenuButton = rootView.findViewById(R.id.quick_start_more); - mDomainRegistrationCta = rootView.findViewById(R.id.my_site_register_domain_cta); - - setupClickListeners(rootView); - - mToolbar = rootView.findViewById(R.id.toolbar_main); - mToolbar.setTitle(R.string.my_site_section_screen_title); - - mToolbar.inflateMenu(R.menu.my_site_menu); - - MenuItem meMenu = mToolbar.getMenu().findItem(R.id.me_item); - View actionView = meMenu.getActionView(); - mAvatarImageView = actionView.findViewById(R.id.avatar); - - actionView.setOnClickListener(item -> ActivityLauncher.viewMeActivityForResult(getActivity())); - - TooltipCompat.setTooltipText(actionView, meMenu.getTitle()); - - return rootView; - } - - private void setupClickListeners(View rootView) { - rootView.findViewById(R.id.site_info_container).setOnClickListener(view -> viewSite()); - - rootView.findViewById(R.id.switch_site).setOnClickListener(v -> showSitePicker()); - - rootView.findViewById(R.id.row_view_site).setOnClickListener(v -> viewSite()); - - mDomainRegistrationCta.setOnClickListener(v -> registerDomain()); - - rootView.findViewById(R.id.quick_action_stats_button).setOnClickListener(v -> { - AnalyticsTracker.track(Stat.QUICK_ACTION_STATS_TAPPED); - viewStats(); - }); - - rootView.findViewById(R.id.row_stats).setOnClickListener(v -> viewStats()); - - mBlavatarImageView.setOnClickListener(v -> updateBlavatar()); - - mPlanContainer.setOnClickListener(v -> { - completeQuickStarTask(QuickStartTask.EXPLORE_PLANS); - ActivityLauncher.viewBlogPlans(getActivity(), getSelectedSite()); - }); - - rootView.findViewById(R.id.quick_action_posts_button).setOnClickListener(v -> { - AnalyticsTracker.track(Stat.QUICK_ACTION_POSTS_TAPPED); - viewPosts(); - }); - - rootView.findViewById(R.id.row_blog_posts).setOnClickListener(v -> viewPosts()); - - rootView.findViewById(R.id.quick_action_media_button).setOnClickListener(v -> { - AnalyticsTracker.track(Stat.QUICK_ACTION_MEDIA_TAPPED); - viewMedia(); - }); - - rootView.findViewById(R.id.row_media).setOnClickListener(v -> viewMedia()); - - rootView.findViewById(R.id.quick_action_pages_button).setOnClickListener(v -> { - AnalyticsTracker.track(Stat.QUICK_ACTION_PAGES_TAPPED); - viewPages(); - }); - - mPageView.setOnClickListener(v -> viewPages()); - - rootView.findViewById(R.id.row_comments).setOnClickListener( - v -> ActivityLauncher.viewCurrentBlogComments(getActivity(), getSelectedSite())); - - mThemesContainer.setOnClickListener(v -> { - completeQuickStarTask(QuickStartTask.CHOOSE_THEME); - if (isQuickStartTaskActive(QuickStartTask.CUSTOMIZE_SITE)) { - requestNextStepOfActiveQuickStartTask(); - } - ActivityLauncher.viewCurrentBlogThemes(getActivity(), getSelectedSite()); - }); - - mPeopleView.setOnClickListener(v -> ActivityLauncher.viewCurrentBlogPeople(getActivity(), getSelectedSite())); - - mPluginsContainer.setOnClickListener( - view -> ActivityLauncher.viewPluginBrowser(getActivity(), getSelectedSite())); - - mActivityLogContainer.setOnClickListener( - view -> ActivityLauncher.viewActivityLogList(getActivity(), getSelectedSite())); - - mSettingsView.setOnClickListener( - v -> ActivityLauncher.viewBlogSettingsForResult(getActivity(), getSelectedSite())); - - mSharingView.setOnClickListener(v -> { - if (isQuickStartTaskActive(QuickStartTask.ENABLE_POST_SHARING)) { - requestNextStepOfActiveQuickStartTask(); - } - ActivityLauncher.viewBlogSharing(getActivity(), getSelectedSite()); - }); - - rootView.findViewById(R.id.row_admin).setOnClickListener( - v -> ActivityLauncher.viewBlogAdmin(getActivity(), getSelectedSite())); - - mActionableEmptyView.button.setOnClickListener( - v -> SitePickerActivity.addSite(getActivity(), mAccountStore.hasAccessToken())); - - mQuickStartCustomizeView.setOnClickListener(v -> showQuickStartList(CUSTOMIZE)); - - mQuickStartGrowView.setOnClickListener(v -> showQuickStartList(GROW)); - - mQuickStartMenuButton.setOnClickListener(v -> showQuickStartCardMenu()); - } - - private void registerDomain() { - AnalyticsUtils - .trackWithSiteDetails(Stat.DOMAIN_CREDIT_REDEMPTION_TAPPED, getSelectedSite()); - ActivityLauncher.viewDomainRegistrationActivityForResult(getActivity(), getSelectedSite(), - DomainRegistrationPurpose.CTA_DOMAIN_CREDIT_REDEMPTION); - } - - private void viewMedia() { - ActivityLauncher.viewCurrentBlogMedia(getActivity(), getSelectedSite()); - } - - private void updateBlavatar() { - AnalyticsTracker.track(Stat.MY_SITE_ICON_TAPPED); - SiteModel site = getSelectedSite(); - if (site != null) { - boolean hasIcon = site.getIconUrl() != null; - if (site.getHasCapabilityManageOptions() && site.getHasCapabilityUploadFiles()) { - if (hasIcon) { - showChangeSiteIconDialog(); - } else { - showAddSiteIconDialog(); - } - completeQuickStarTask(QuickStartTask.UPLOAD_SITE_ICON); - } else { - showEditingSiteIconRequiresPermissionDialog( - hasIcon ? getString(R.string.my_site_icon_dialog_change_requires_permission_message) - : getString(R.string.my_site_icon_dialog_add_requires_permission_message)); - } - } - } - - private void viewPosts() { - requestNextStepOfActiveQuickStartTask(); - SiteModel selectedSite = getSelectedSite(); - if (selectedSite != null) { - ActivityLauncher.viewCurrentBlogPosts(requireActivity(), selectedSite); - } else { - ToastUtils.showToast(getActivity(), R.string.site_cannot_be_loaded); - } - } - - private void viewPages() { - requestNextStepOfActiveQuickStartTask(); - SiteModel selectedSite = getSelectedSite(); - if (selectedSite != null) { - ActivityLauncher.viewCurrentBlogPages(requireActivity(), selectedSite); - } else { - ToastUtils.showToast(getActivity(), R.string.site_cannot_be_loaded); - } - } - - private void viewStats() { - SiteModel selectedSite = getSelectedSite(); - if (selectedSite != null) { - completeQuickStarTask(QuickStartTask.CHECK_STATS); - if (!mAccountStore.hasAccessToken() && selectedSite.isJetpackConnected()) { - // If the user is not connected to WordPress.com, ask him to connect first. - startWPComLoginForJetpackStats(); - } else if (selectedSite.isWPCom() || (selectedSite.isJetpackInstalled() && selectedSite - .isJetpackConnected())) { - ActivityLauncher.viewBlogStats(getActivity(), selectedSite); - } else { - ActivityLauncher.viewConnectJetpackForStats(getActivity(), selectedSite); - } - } - } - - private void viewSite() { - completeQuickStarTask(QuickStartTask.VIEW_SITE); - ActivityLauncher.viewCurrentSite(getActivity(), getSelectedSite(), true); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - if (mActiveTutorialPrompt != null) { - showQuickStartFocusPoint(); - } - } - - private void updateQuickStartContainer() { - if (!isAdded()) { - return; - } - if (QuickStartUtils.isQuickStartInProgress(mQuickStartStore)) { - int site = AppPrefs.getSelectedSite(); - - int countCustomizeCompleted = mQuickStartStore.getCompletedTasksByType(site, CUSTOMIZE).size(); - int countCustomizeUncompleted = mQuickStartStore.getUncompletedTasksByType(site, CUSTOMIZE).size(); - int countGrowCompleted = mQuickStartStore.getCompletedTasksByType(site, GROW).size(); - int countGrowUncompleted = mQuickStartStore.getUncompletedTasksByType(site, GROW).size(); - - if (countCustomizeUncompleted > 0) { - mQuickStartCustomizeIcon.setEnabled(true); - mQuickStartCustomizeTitle.setEnabled(true); - mQuickStartCustomizeTitle.setPaintFlags( - mQuickStartCustomizeTitle.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); - } else { - mQuickStartCustomizeIcon.setEnabled(false); - mQuickStartCustomizeTitle.setEnabled(false); - mQuickStartCustomizeTitle.setPaintFlags( - mQuickStartCustomizeTitle.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } - - mQuickStartCustomizeSubtitle.setText(getString(R.string.quick_start_sites_type_subtitle, - countCustomizeCompleted, countCustomizeCompleted + countCustomizeUncompleted)); - - if (countGrowUncompleted > 0) { - mQuickStartGrowIcon.setBackgroundResource(R.drawable.bg_oval_pink_50_multiple_users_white_40dp); - mQuickStartGrowTitle.setEnabled(true); - mQuickStartGrowTitle.setPaintFlags( - mQuickStartGrowTitle.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); - } else { - mQuickStartGrowIcon.setBackgroundResource(R.drawable.bg_oval_neutral_30_multiple_users_white_40dp); - mQuickStartGrowTitle.setEnabled(false); - mQuickStartGrowTitle.setPaintFlags( - mQuickStartGrowTitle.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } - - mQuickStartGrowSubtitle.setText(getString(R.string.quick_start_sites_type_subtitle, - countGrowCompleted, countGrowCompleted + countGrowUncompleted)); - - mQuickStartContainer.setVisibility(View.VISIBLE); - } else { - mQuickStartContainer.setVisibility(View.GONE); - } - } - - private void showQuickStartCardMenu() { - PopupMenu quickStartPopupMenu = new PopupMenu(requireContext(), mQuickStartMenuButton); - quickStartPopupMenu.setOnMenuItemClickListener(item -> { - if (item.getItemId() == R.id.quick_start_card_menu_remove) { - showRemoveNextStepsDialog(); - return true; - } - return false; - }); - quickStartPopupMenu.inflate(R.menu.quick_start_card_menu); - quickStartPopupMenu.show(); - } - - private void showQuickStartList(QuickStartTaskType type) { - clearActiveQuickStart(); - final Bundle bundle = QuickStartFullScreenDialogFragment.newBundle(type); - - switch (type) { - case CUSTOMIZE: - new FullScreenDialogFragment.Builder(requireContext()) - .setTitle(R.string.quick_start_sites_type_customize) - .setOnConfirmListener(this) - .setOnDismissListener(this) - .setContent(QuickStartFullScreenDialogFragment.class, bundle) - .build() - .show(requireActivity().getSupportFragmentManager(), FullScreenDialogFragment.TAG); - break; - case GROW: - new FullScreenDialogFragment.Builder(requireContext()) - .setTitle(R.string.quick_start_sites_type_grow) - .setOnConfirmListener(this) - .setOnDismissListener(this) - .setContent(QuickStartFullScreenDialogFragment.class, bundle) - .build() - .show(requireActivity().getSupportFragmentManager(), FullScreenDialogFragment.TAG); - break; - case UNKNOWN: - break; - } - } - - private void showAddSiteIconDialog() { - BasicFragmentDialog dialog = new BasicFragmentDialog(); - String tag = TAG_ADD_SITE_ICON_DIALOG; - dialog.initialize(tag, getString(R.string.my_site_icon_dialog_title), - getString(R.string.my_site_icon_dialog_add_message), - getString(R.string.yes), - getString(R.string.no), - null); - dialog.show((requireActivity()).getSupportFragmentManager(), tag); - } - - private void showChangeSiteIconDialog() { - BasicFragmentDialog dialog = new BasicFragmentDialog(); - String tag = TAG_CHANGE_SITE_ICON_DIALOG; - dialog.initialize(tag, getString(R.string.my_site_icon_dialog_title), - getString(R.string.my_site_icon_dialog_change_message), - getString(R.string.my_site_icon_dialog_change_button), - getString(R.string.my_site_icon_dialog_remove_button), - getString(R.string.my_site_icon_dialog_cancel_button)); - dialog.show((requireActivity()).getSupportFragmentManager(), tag); - } - - private void showEditingSiteIconRequiresPermissionDialog(@NonNull String message) { - BasicFragmentDialog dialog = new BasicFragmentDialog(); - String tag = TAG_EDIT_SITE_ICON_PERMISSIONS_DIALOG; - dialog.initialize(tag, getString(R.string.my_site_icon_dialog_title), - message, - getString(R.string.dialog_button_ok), - null, - null); - dialog.show((requireActivity()).getSupportFragmentManager(), tag); - } - - private void showRemoveNextStepsDialog() { - BasicFragmentDialog dialog = new BasicFragmentDialog(); - String tag = TAG_REMOVE_NEXT_STEPS_DIALOG; - dialog.initialize(tag, getString(R.string.quick_start_dialog_remove_next_steps_title), - getString(R.string.quick_start_dialog_remove_next_steps_message), - getString(R.string.remove), - getString(R.string.cancel), - null); - dialog.show((requireActivity()).getSupportFragmentManager(), tag); - } - - private void startWPComLoginForJetpackStats() { - Intent loginIntent = new Intent(getActivity(), LoginActivity.class); - LoginMode.JETPACK_STATS.putInto(loginIntent); - startActivityForResult(loginIntent, RequestCodes.DO_LOGIN); - } - - private void showSitePicker() { - if (isAdded()) { - ActivityLauncher.showSitePickerForResult(getActivity(), getSelectedSite()); - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - switch (requestCode) { - case RequestCodes.DO_LOGIN: - if (resultCode == Activity.RESULT_OK) { - ActivityLauncher.viewBlogStats(getActivity(), getSelectedSite()); - } - break; - case RequestCodes.SITE_PICKER: - if (resultCode == Activity.RESULT_OK) { - // reset comments status filter - AppPrefs.setCommentsStatusFilter(CommentStatusCriteria.ALL); - // reset domain credit flag - it will be checked in onSiteChanged - mIsDomainCreditAvailable = false; - } - break; - case RequestCodes.PHOTO_PICKER: - if (resultCode == Activity.RESULT_OK && data != null) { - if (data.hasExtra(PhotoPickerActivity.EXTRA_MEDIA_ID)) { - int mediaId = (int) data.getLongExtra(PhotoPickerActivity.EXTRA_MEDIA_ID, 0); - - showSiteIconProgressBar(true); - updateSiteIconMediaId(mediaId); - } else { - String[] mediaUriStringsArray = data.getStringArrayExtra(PhotoPickerActivity.EXTRA_MEDIA_URIS); - if (mediaUriStringsArray == null || mediaUriStringsArray.length == 0) { - AppLog.e(AppLog.T.UTILS, "Can't resolve picked or captured image"); - return; - } - - PhotoPickerMediaSource source = PhotoPickerMediaSource.fromString( - data.getStringExtra(PhotoPickerActivity.EXTRA_MEDIA_SOURCE)); - - AnalyticsTracker.Stat stat = - source == PhotoPickerMediaSource.ANDROID_CAMERA - ? AnalyticsTracker.Stat.MY_SITE_ICON_SHOT_NEW - : AnalyticsTracker.Stat.MY_SITE_ICON_GALLERY_PICKED; - AnalyticsTracker.track(stat); - - Uri imageUri = Uri.parse(mediaUriStringsArray[0]); - if (imageUri != null) { - boolean didGoWell = WPMediaUtils.fetchMediaAndDoNext(getActivity(), imageUri, - uri -> { - showSiteIconProgressBar(true); - startCropActivity(uri); - }); - - if (!didGoWell) { - AppLog.e(AppLog.T.UTILS, "Can't download picked or captured image"); - } - } - } - } - break; - case UCrop.REQUEST_CROP: - if (resultCode == Activity.RESULT_OK) { - AnalyticsTracker.track(Stat.MY_SITE_ICON_CROPPED); - WPMediaUtils.fetchMediaAndDoNext(getActivity(), UCrop.getOutput(data), - uri -> startSiteIconUpload( - MediaUtils.getRealPathFromURI(getActivity(), uri))); - } else if (resultCode == UCrop.RESULT_ERROR) { - AppLog.e(AppLog.T.MAIN, "Image cropping failed!", UCrop.getError(data)); - ToastUtils.showToast(getActivity(), R.string.error_cropping_image, Duration.SHORT); - } - break; - case RequestCodes.DOMAIN_REGISTRATION: - if (resultCode == Activity.RESULT_OK && isAdded() && data != null) { - AnalyticsTracker.track(Stat.DOMAIN_CREDIT_REDEMPTION_SUCCESS); - String email = data.getStringExtra(DomainRegistrationResultFragment.RESULT_REGISTERED_DOMAIN_EMAIL); - requestEmailValidation(getContext(), email); - } - break; - } - } - - @Override - public void onConfirm(@Nullable Bundle result) { - if (result != null) { - QuickStartTask task = (QuickStartTask) result.getSerializable(RESULT_TASK); - if (task == null || task == QuickStartTask.CREATE_SITE) { - return; - } - - // Remove existing quick start indicator, if necessary. - if (mActiveTutorialPrompt != null) { - removeQuickStartFocusPoint(); - } - - mActiveTutorialPrompt = QuickStartMySitePrompts.getPromptDetailsForTask(task); - showActiveQuickStartTutorial(); - } - } - - @Override - public void onDismiss() { - updateQuickStartContainer(); - } - - private void startSiteIconUpload(final String filePath) { - if (TextUtils.isEmpty(filePath)) { - ToastUtils.showToast(getActivity(), R.string.error_locating_image, ToastUtils.Duration.SHORT); - return; - } - - File file = new File(filePath); - if (!file.exists()) { - ToastUtils.showToast(getActivity(), R.string.file_error_create, ToastUtils.Duration.SHORT); - return; - } - - SiteModel site = getSelectedSite(); - if (site != null) { - MediaModel media = buildMediaModel(file, site); - if (media == null) { - ToastUtils.showToast(getActivity(), R.string.file_not_found, ToastUtils.Duration.SHORT); - return; - } - UploadService.uploadMedia(getActivity(), media); - } else { - ToastUtils.showToast(getActivity(), R.string.error_generic, ToastUtils.Duration.SHORT); - AppLog.e(T.MAIN, "Unexpected error - Site icon upload failed, because there wasn't any site selected."); - } - } - - private void showSiteIconProgressBar(boolean isVisible) { - if (mBlavatarProgressBar != null && mBlavatarImageView != null) { - if (isVisible) { - mBlavatarProgressBar.setVisibility(View.VISIBLE); - mBlavatarImageView.setVisibility(View.INVISIBLE); - } else { - mBlavatarProgressBar.setVisibility(View.GONE); - mBlavatarImageView.setVisibility(View.VISIBLE); - } - } - } - - private boolean isMediaUploadInProgress() { - return mBlavatarProgressBar.getVisibility() == View.VISIBLE; - } - - private MediaModel buildMediaModel(File file, SiteModel site) { - Uri uri = new Uri.Builder().path(file.getPath()).build(); - String mimeType = requireActivity().getContentResolver().getType(uri); - return FluxCUtils.mediaModelFromLocalUri(requireActivity(), uri, mimeType, mMediaStore, site.getId()); - } - - private void startCropActivity(Uri uri) { - final Context context = getActivity(); - - if (context == null) { - return; - } - - UCrop.Options options = new UCrop.Options(); - options.setShowCropGrid(false); - options.setStatusBarColor(ContextCompat.getColor(context, R.color.status_bar)); - options.setToolbarColor(ContextCompat.getColor(context, R.color.primary)); - options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.NONE, UCropActivity.NONE); - options.setHideBottomControls(true); - - UCrop.of(uri, Uri.fromFile(new File(context.getCacheDir(), "cropped_for_site_icon.jpg"))) - .withAspectRatio(1, 1) - .withOptions(options) - .start(getActivity(), this); - } - - private void refreshSelectedSiteDetails(SiteModel site) { - if (!isAdded() || getView() == null) { - return; - } - - if (site == null) { - mScrollView.setVisibility(View.GONE); - mActionableEmptyView.setVisibility(View.VISIBLE); - - // Hide actionable empty view image when screen height is under 600 pixels. - if (DisplayUtils.getDisplayPixelHeight(getActivity()) >= 600) { - mActionableEmptyView.image.setVisibility(View.VISIBLE); - } else { - mActionableEmptyView.image.setVisibility(View.GONE); - } - - return; - } - - if (SiteUtils.onFreePlan(site) || SiteUtils.hasCustomDomain(site)) { - mIsDomainCreditAvailable = false; - toggleDomainRegistrationCtaVisibility(); - } else if (!mIsDomainCreditChecked) { - fetchSitePlans(site); - } else { - toggleDomainRegistrationCtaVisibility(); - } - - mScrollView.setVisibility(View.VISIBLE); - mActionableEmptyView.setVisibility(View.GONE); - - toggleAdminVisibility(site); - - int themesVisibility = ThemeBrowserActivity.isAccessible(site) ? View.VISIBLE : View.GONE; - mLookAndFeelHeader.setVisibility(themesVisibility); - mThemesContainer.setVisibility(themesVisibility); - - // sharing is only exposed for sites accessed via the WPCOM REST API (wpcom or Jetpack) - int sharingVisibility = SiteUtils.isAccessedViaWPComRest(site) ? View.VISIBLE : View.GONE; - mSharingView.setVisibility(sharingVisibility); - - // show settings for all self-hosted to expose Delete Site - boolean isAdminOrSelfHosted = site.getHasCapabilityManageOptions() || !SiteUtils.isAccessedViaWPComRest(site); - mSettingsView.setVisibility(isAdminOrSelfHosted ? View.VISIBLE : View.GONE); - mPeopleView.setVisibility(site.getHasCapabilityListUsers() ? View.VISIBLE : View.GONE); - - mPluginsContainer.setVisibility(PluginUtils.isPluginFeatureAvailable(site) ? View.VISIBLE : View.GONE); - - // if either people or settings is visible, configuration header should be visible - int settingsVisibility = (isAdminOrSelfHosted || site.getHasCapabilityListUsers()) ? View.VISIBLE : View.GONE; - mConfigurationHeader.setVisibility(settingsVisibility); - - mImageManager.load(mBlavatarImageView, ImageType.BLAVATAR, SiteUtils.getSiteIconUrl(site, mBlavatarSz)); - String homeUrl = SiteUtils.getHomeURLOrHostName(site); - String blogTitle = SiteUtils.getSiteNameOrHomeURL(site); - - mBlogTitleTextView.setText(blogTitle); - mBlogSubtitleTextView.setText(homeUrl); - - // Hide the Plan item if the Plans feature is not available for this blog - String planShortName = site.getPlanShortName(); - if (!TextUtils.isEmpty(planShortName) && site.getHasCapabilityManageOptions()) { - if (site.isWPCom() || site.isAutomatedTransfer()) { - mCurrentPlanNameTextView.setText(planShortName); - mPlanContainer.setVisibility(View.VISIBLE); - } else { - // TODO: Support Jetpack plans - mPlanContainer.setVisibility(View.GONE); - } - } else { - mPlanContainer.setVisibility(View.GONE); - } - - // Do not show pages menu item to Collaborators. - int pageVisibility = site.isSelfHostedAdmin() || site.getHasCapabilityEditPages() ? View.VISIBLE : View.GONE; - mPageView.setVisibility(pageVisibility); - mQuickActionPageButtonContainer.setVisibility(pageVisibility); - - if (pageVisibility == View.VISIBLE) { - mQuickActionButtonsContainer.setWeightSum(100f); - } else { - mQuickActionButtonsContainer.setWeightSum(75f); - } - } - - private void toggleAdminVisibility(@Nullable final SiteModel site) { - if (site == null) { - return; - } - if (shouldHideWPAdmin(site)) { - mAdminView.setVisibility(View.GONE); - } else { - mAdminView.setVisibility(View.VISIBLE); - } - } - - private boolean shouldHideWPAdmin(@Nullable final SiteModel site) { - if (site == null) { - return false; - } - if (!site.isWPCom()) { - return false; - } else { - Date dateCreated = DateTimeUtils.dateFromIso8601(mAccountStore.getAccount().getDate()); - GregorianCalendar calendar = new GregorianCalendar(HIDE_WP_ADMIN_YEAR, HIDE_WP_ADMIN_MONTH, - HIDE_WP_ADMIN_DAY); - calendar.setTimeZone(TimeZone.getTimeZone(HIDE_WP_ADMIN_GMT_TIME_ZONE)); - return dateCreated != null && dateCreated.after(calendar.getTime()); - } - } - - @Override - public void onScrollToTop() { - if (isAdded()) { - mScrollView.smoothScrollTo(0, 0); - } - } - - @Override - public void onStop() { - mDispatcher.unregister(this); - EventBus.getDefault().unregister(this); - super.onStop(); - } - - @Override - public void onStart() { - super.onStart(); - mDispatcher.register(this); - EventBus.getDefault().register(this); - } - - /** - * We can't just use fluxc OnSiteChanged event, as the order of events is not guaranteed -> getSelectedSite() - * method might return an out of date SiteModel, if the OnSiteChanged event handler in the WPMainActivity wasn't - * called yet. - */ - public void onSiteChanged(SiteModel site) { - // whenever site changes we hide CTA and check for credit in refreshSelectedSiteDetails() - mIsDomainCreditChecked = false; - - refreshSelectedSiteDetails(site); - showSiteIconProgressBar(false); - } - - @SuppressWarnings("unused") - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(UploadService.UploadErrorEvent event) { - AnalyticsTracker.track(Stat.MY_SITE_ICON_UPLOAD_UNSUCCESSFUL); - EventBus.getDefault().removeStickyEvent(event); - - if (isMediaUploadInProgress()) { - showSiteIconProgressBar(false); - } - - SiteModel site = getSelectedSite(); - if (site != null && event.post != null) { - if (event.post.getLocalSiteId() == site.getId()) { - mUploadUtilsWrapper.onPostUploadedSnackbarHandler(getActivity(), - requireActivity().findViewById(R.id.coordinator), true, - event.post, event.errorMessage, site); - } - } else if (event.mediaModelList != null && !event.mediaModelList.isEmpty()) { - mUploadUtilsWrapper.onMediaUploadedSnackbarHandler(getActivity(), - requireActivity().findViewById(R.id.coordinator), true, - event.mediaModelList, site, event.errorMessage); - } - } - - @SuppressWarnings("unused") - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(UploadService.UploadMediaSuccessEvent event) { - AnalyticsTracker.track(Stat.MY_SITE_ICON_UPLOADED); - EventBus.getDefault().removeStickyEvent(event); - SiteModel site = getSelectedSite(); - - if (site != null) { - if (isMediaUploadInProgress()) { - if (event.mediaModelList.size() > 0) { - MediaModel media = event.mediaModelList.get(0); - mImageManager.load(mBlavatarImageView, ImageType.BLAVATAR, PhotonUtils - .getPhotonImageUrl(media.getUrl(), mBlavatarSz, mBlavatarSz, PhotonUtils.Quality.HIGH, - site.isPrivateWPComAtomic())); - updateSiteIconMediaId((int) media.getMediaId()); - } else { - AppLog.w(T.MAIN, "Site icon upload completed, but mediaList is empty."); - } - showSiteIconProgressBar(false); - } else { - if (event.mediaModelList != null && !event.mediaModelList.isEmpty()) { - mUploadUtilsWrapper.onMediaUploadedSnackbarHandler(getActivity(), - requireActivity().findViewById(R.id.coordinator), false, - event.mediaModelList, site, event.successMessage); - } - } - } - } - - @Override - public void onPositiveClicked(@NonNull String instanceTag) { - switch (instanceTag) { - case TAG_ADD_SITE_ICON_DIALOG: - case TAG_CHANGE_SITE_ICON_DIALOG: - ActivityLauncher.showPhotoPickerForResult(getActivity(), - MediaBrowserType.SITE_ICON_PICKER, getSelectedSite(), null); - break; - case TAG_EDIT_SITE_ICON_PERMISSIONS_DIALOG: - // no-op - break; - case TAG_QUICK_START_DIALOG: - startQuickStart(); - AnalyticsTracker.track(Stat.QUICK_START_REQUEST_DIALOG_POSITIVE_TAPPED); - break; - case TAG_QUICK_START_MIGRATION_DIALOG: - AnalyticsTracker.track(Stat.QUICK_START_MIGRATION_DIALOG_POSITIVE_TAPPED); - break; - case TAG_REMOVE_NEXT_STEPS_DIALOG: - AnalyticsTracker.track(Stat.QUICK_START_REMOVE_DIALOG_POSITIVE_TAPPED); - skipQuickStart(); - updateQuickStartContainer(); - clearActiveQuickStart(); - break; - default: - AppLog.e(T.EDITOR, "Dialog instanceTag is not recognized"); - throw new UnsupportedOperationException("Dialog instanceTag is not recognized"); - } - } - - private void skipQuickStart() { - int siteId = AppPrefs.getSelectedSite(); - for (QuickStartTask quickStartTask : QuickStartTask.values()) { - mQuickStartStore.setDoneTask(siteId, quickStartTask, true); - } - mQuickStartStore.setQuickStartCompleted(siteId, true); - // skipping all tasks means no achievement notification, so we mark it as received - mQuickStartStore.setQuickStartNotificationReceived(siteId, true); - } - - private void startQuickStart() { - mQuickStartStore.setDoneTask(AppPrefs.getSelectedSite(), QuickStartTask.CREATE_SITE, true); - updateQuickStartContainer(); - } - - private void toggleDomainRegistrationCtaVisibility() { - if (mIsDomainCreditAvailable) { - // we nest this check because of some weirdness with ui state and race conditions - if (mDomainRegistrationCta.getVisibility() != View.VISIBLE) { - AnalyticsTracker.track(Stat.DOMAIN_CREDIT_PROMPT_SHOWN); - mDomainRegistrationCta.setVisibility(View.VISIBLE); - } - } else { - mDomainRegistrationCta.setVisibility(View.GONE); - } - } - - @Override - public void onNegativeClicked(@NonNull String instanceTag) { - switch (instanceTag) { - case TAG_ADD_SITE_ICON_DIALOG: - showQuickStartNoticeIfNecessary(); - break; - case TAG_CHANGE_SITE_ICON_DIALOG: - AnalyticsTracker.track(Stat.MY_SITE_ICON_REMOVED); - showSiteIconProgressBar(true); - updateSiteIconMediaId(0); - break; - case TAG_QUICK_START_DIALOG: - AnalyticsTracker.track(Stat.QUICK_START_REQUEST_DIALOG_NEGATIVE_TAPPED); - break; - case TAG_REMOVE_NEXT_STEPS_DIALOG: - AnalyticsTracker.track(Stat.QUICK_START_REMOVE_DIALOG_NEGATIVE_TAPPED); - break; - default: - AppLog.e(T.EDITOR, "Dialog instanceTag '" + instanceTag + "' is not recognized"); - throw new UnsupportedOperationException("Dialog instanceTag is not recognized"); - } - } - - @Override - public void onNeutralClicked(@NonNull String instanceTag) { - if (TAG_QUICK_START_DIALOG.equals(instanceTag)) { - AppPrefs.setQuickStartDisabled(true); - AnalyticsTracker.track(Stat.QUICK_START_REQUEST_DIALOG_NEUTRAL_TAPPED); - } else { - AppLog.e(T.EDITOR, "Dialog instanceTag '" + instanceTag + "' is not recognized"); - throw new UnsupportedOperationException("Dialog instanceTag is not recognized"); - } - } - - @Override - public void onDismissByOutsideTouch(@NotNull String instanceTag) { - switch (instanceTag) { - case TAG_ADD_SITE_ICON_DIALOG: - showQuickStartNoticeIfNecessary(); - break; - case TAG_CHANGE_SITE_ICON_DIALOG: - case TAG_EDIT_SITE_ICON_PERMISSIONS_DIALOG: - case TAG_QUICK_START_DIALOG: - case TAG_QUICK_START_MIGRATION_DIALOG: - case TAG_REMOVE_NEXT_STEPS_DIALOG: - break; // do nothing - default: - AppLog.e(T.EDITOR, "Dialog instanceTag '" + instanceTag + "' is not recognized"); - throw new UnsupportedOperationException("Dialog instanceTag is not recognized"); - } - } - - @Override - public void onLinkClicked(@NonNull String instanceTag) { - } - - @Override - public void onSettingsSaved() { - // refresh the site after site icon change - SiteModel site = getSelectedSite(); - if (site != null) { - mDispatcher.dispatch(SiteActionBuilder.newFetchSiteAction(site)); - } - } - - @Override - public void onSaveError(Exception error) { - showSiteIconProgressBar(false); - } - - @Override - public void onFetchError(Exception error) { - showSiteIconProgressBar(false); - } - - @Override - public void onSettingsUpdated() { - } - - @Override - public void onCredentialsValidated(Exception error) { - } - - private void fetchSitePlans(@Nullable SiteModel site) { - mDispatcher.dispatch(SiteActionBuilder.newFetchPlansAction(site)); - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onPlansFetched(OnPlansFetched event) { - if (AppPrefs.getSelectedSite() != event.site.getId()) { - return; - } - - if (event.isError()) { - AppLog.e(T.DOMAIN_REGISTRATION, "An error occurred while fetching plans : " + event.error.message); - } else { - mIsDomainCreditChecked = true; - mIsDomainCreditAvailable = isDomainCreditAvailable(event.plans); - toggleDomainRegistrationCtaVisibility(); - } - } - - private Runnable mAddQuickStartFocusPointTask = new Runnable() { - @Override - public void run() { - // technically there is no situation (yet) where fragment is not added but we need to show focus point - if (!isAdded()) { - return; - } - - ViewGroup parentView = requireActivity().findViewById(mActiveTutorialPrompt.getParentContainerId()); - final View quickStartTarget = requireActivity().findViewById(mActiveTutorialPrompt.getFocusedContainerId()); - - if (quickStartTarget == null || parentView == null) { - return; - } - - int focusPointSize = getResources().getDimensionPixelOffset(R.dimen.quick_start_focus_point_size); - int horizontalOffset; - int verticalOffset; - - if (QuickStartMySitePrompts.isTargetingBottomNavBar(mActiveTutorialPrompt.getTask())) { - horizontalOffset = (quickStartTarget.getWidth() / 2) - focusPointSize + getResources() - .getDimensionPixelOffset(R.dimen.quick_start_focus_point_bottom_nav_offset); - verticalOffset = 0; - } else if (mActiveTutorialPrompt.getTask() == QuickStartTask.UPLOAD_SITE_ICON) { - horizontalOffset = focusPointSize; - verticalOffset = -focusPointSize / 2; - } else { - horizontalOffset = - getResources().getDimensionPixelOffset(R.dimen.quick_start_focus_point_my_site_right_offset); - verticalOffset = (((quickStartTarget.getHeight()) - focusPointSize) / 2); - } - - QuickStartUtils.addQuickStartFocusPointAboveTheView(parentView, quickStartTarget, horizontalOffset, - verticalOffset); - - // highlight MySite row and scroll to it - if (!QuickStartMySitePrompts.isTargetingBottomNavBar(mActiveTutorialPrompt.getTask())) { - mScrollView.post(() -> mScrollView.smoothScrollTo(0, quickStartTarget.getTop())); - } - } - }; - - private void showQuickStartFocusPoint() { - if (getView() == null || !hasActiveQuickStartTask()) { - return; - } - getView().post(mAddQuickStartFocusPointTask); - } - - private void removeQuickStartFocusPoint() { - if (getView() == null || !isAdded()) { - return; - } - getView().removeCallbacks(mAddQuickStartFocusPointTask); - QuickStartUtils.removeQuickStartFocusPoint(requireActivity().findViewById(R.id.root_view_main)); - } - - boolean isQuickStartTaskActive(QuickStartTask task) { - return hasActiveQuickStartTask() && mActiveTutorialPrompt.getTask() == task; - } - - private void completeQuickStarTask(QuickStartTask quickStartTask) { - if (getSelectedSite() != null) { - // we need to process notices for tasks that are completed at MySite fragment - AppPrefs.setQuickStartNoticeRequired( - !mQuickStartStore.hasDoneTask(AppPrefs.getSelectedSite(), quickStartTask) - && mActiveTutorialPrompt != null - && mActiveTutorialPrompt.getTask() == quickStartTask); - - QuickStartUtils.completeTaskAndRemindNextOne(mQuickStartStore, quickStartTask, mDispatcher, - getSelectedSite(), getContext()); - // We update completed tasks counter onResume, but UPLOAD_SITE_ICON can be completed without navigating - // away from the activity, so we are updating counter here - if (quickStartTask == QuickStartTask.UPLOAD_SITE_ICON) { - updateQuickStartContainer(); - } - if (mActiveTutorialPrompt != null && mActiveTutorialPrompt.getTask() == quickStartTask) { - removeQuickStartFocusPoint(); - clearActiveQuickStartTask(); - } - } - } - - private void clearActiveQuickStart() { - // Clear pressed row. - if (mActiveTutorialPrompt != null - && !QuickStartMySitePrompts.isTargetingBottomNavBar(mActiveTutorialPrompt.getTask())) { - requireActivity().findViewById(mActiveTutorialPrompt.getFocusedContainerId()).setPressed(false); - } - - if (getActivity() != null && !getActivity().isChangingConfigurations()) { - clearActiveQuickStartTask(); - removeQuickStartFocusPoint(); - } - - mQuickStartSnackBarHandler.removeCallbacksAndMessages(null); - } - - void requestNextStepOfActiveQuickStartTask() { - if (!hasActiveQuickStartTask()) { - return; - } - removeQuickStartFocusPoint(); - EventBus.getDefault().postSticky(new QuickStartEvent(mActiveTutorialPrompt.getTask())); - clearActiveQuickStartTask(); - } - - private void clearActiveQuickStartTask() { - mActiveTutorialPrompt = null; - } - - private boolean hasActiveQuickStartTask() { - return mActiveTutorialPrompt != null; - } - - private void showActiveQuickStartTutorial() { - if (!hasActiveQuickStartTask() || !isAdded() || !(getActivity() instanceof WPMainActivity)) { - return; - } - - showQuickStartFocusPoint(); - - Spannable shortQuickStartMessage = QuickStartUtils.stylizeQuickStartPrompt(getActivity(), - mActiveTutorialPrompt.getShortMessagePrompt(), - mActiveTutorialPrompt.getIconId()); - - WPDialogSnackbar promptSnackbar = WPDialogSnackbar.make(requireActivity().findViewById(R.id.coordinator), - shortQuickStartMessage, getResources().getInteger(R.integer.quick_start_snackbar_duration_ms)); - - ((WPMainActivity) getActivity()).showQuickStartSnackBar(promptSnackbar); - } - - private void showQuickStartDialogMigration() { - PromoDialog promoDialog = new PromoDialog(); - promoDialog.initialize( - TAG_QUICK_START_MIGRATION_DIALOG, - getString(R.string.quick_start_dialog_migration_title), - getString(R.string.quick_start_dialog_migration_message), - getString(android.R.string.ok), - R.drawable.img_illustration_checkmark_280dp, - "", - "", - ""); - - if (getFragmentManager() != null) { - promoDialog.show(getFragmentManager(), TAG_QUICK_START_MIGRATION_DIALOG); - AppPrefs.setQuickStartMigrationDialogShown(true); - AnalyticsTracker.track(Stat.QUICK_START_MIGRATION_DIALOG_VIEWED); - } - } - - private void updateSiteIconMediaId(int mediaId) { - if (mSiteSettings != null) { - mSiteSettings.setSiteIconMediaId(mediaId); - mSiteSettings.saveSettings(); - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt new file mode 100644 index 000000000000..8bcab019a61b --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt @@ -0,0 +1,1374 @@ +package org.wordpress.android.ui.main + +import android.app.Activity +import android.content.Intent +import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.appcompat.widget.TooltipCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.yalantis.ucrop.UCrop +import com.yalantis.ucrop.UCrop.Options +import com.yalantis.ucrop.UCropActivity +import kotlinx.android.synthetic.main.me_action_layout.* +import kotlinx.android.synthetic.main.my_site_fragment.* +import kotlinx.android.synthetic.main.toolbar_main.* +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAIN_CREDIT_PROMPT_SHOWN +import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAIN_CREDIT_REDEMPTION_SUCCESS +import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAIN_CREDIT_REDEMPTION_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.MY_SITE_ICON_CROPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.MY_SITE_ICON_GALLERY_PICKED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.MY_SITE_ICON_REMOVED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.MY_SITE_ICON_SHOT_NEW +import org.wordpress.android.analytics.AnalyticsTracker.Stat.MY_SITE_ICON_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.MY_SITE_ICON_UPLOADED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.MY_SITE_ICON_UPLOAD_UNSUCCESSFUL +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_ACTION_MEDIA_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_ACTION_PAGES_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_ACTION_POSTS_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_ACTION_STATS_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_START_MIGRATION_DIALOG_POSITIVE_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_START_MIGRATION_DIALOG_VIEWED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_START_REMOVE_DIALOG_NEGATIVE_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_START_REMOVE_DIALOG_POSITIVE_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_START_REQUEST_DIALOG_NEGATIVE_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_START_REQUEST_DIALOG_NEUTRAL_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_START_REQUEST_DIALOG_POSITIVE_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_START_TASK_DIALOG_NEGATIVE_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_START_TASK_DIALOG_POSITIVE_TAPPED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.QUICK_START_TASK_DIALOG_VIEWED +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.generated.SiteActionBuilder +import org.wordpress.android.fluxc.model.MediaModel +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.fluxc.store.MediaStore +import org.wordpress.android.fluxc.store.QuickStartStore +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask.CHECK_STATS +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask.CHOOSE_THEME +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask.CREATE_SITE +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask.CUSTOMIZE_SITE +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask.ENABLE_POST_SHARING +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask.EXPLORE_PLANS +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask.UPLOAD_SITE_ICON +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask.VIEW_SITE +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType.CUSTOMIZE +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType.GROW +import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType.UNKNOWN +import org.wordpress.android.fluxc.store.SiteStore.OnPlansFetched +import org.wordpress.android.login.LoginMode.JETPACK_STATS +import org.wordpress.android.ui.ActivityLauncher +import org.wordpress.android.ui.FullScreenDialogFragment +import org.wordpress.android.ui.FullScreenDialogFragment.Builder +import org.wordpress.android.ui.FullScreenDialogFragment.OnConfirmListener +import org.wordpress.android.ui.FullScreenDialogFragment.OnDismissListener +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.ui.accounts.LoginActivity +import org.wordpress.android.ui.comments.CommentsListFragment.CommentStatusCriteria.ALL +import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose.CTA_DOMAIN_CREDIT_REDEMPTION +import org.wordpress.android.ui.domains.DomainRegistrationResultFragment +import org.wordpress.android.ui.main.WPMainActivity.OnScrollToTopListener +import org.wordpress.android.ui.main.utils.MeGravatarLoader +import org.wordpress.android.ui.media.MediaBrowserType.SITE_ICON_PICKER +import org.wordpress.android.ui.photopicker.PhotoPickerActivity +import org.wordpress.android.ui.photopicker.PhotoPickerActivity.PhotoPickerMediaSource +import org.wordpress.android.ui.photopicker.PhotoPickerActivity.PhotoPickerMediaSource.ANDROID_CAMERA +import org.wordpress.android.ui.plans.isDomainCreditAvailable +import org.wordpress.android.ui.plugins.PluginUtils +import org.wordpress.android.ui.posts.BasicFragmentDialog +import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogNegativeClickInterface +import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogOnDismissByOutsideTouchInterface +import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogPositiveClickInterface +import org.wordpress.android.ui.posts.PromoDialog +import org.wordpress.android.ui.posts.PromoDialog.PromoDialogClickInterface +import org.wordpress.android.ui.prefs.AppPrefs +import org.wordpress.android.ui.prefs.SiteSettingsInterface +import org.wordpress.android.ui.prefs.SiteSettingsInterface.SiteSettingsListener +import org.wordpress.android.ui.quickstart.QuickStartEvent +import org.wordpress.android.ui.quickstart.QuickStartFullScreenDialogFragment +import org.wordpress.android.ui.quickstart.QuickStartMySitePrompts +import org.wordpress.android.ui.quickstart.QuickStartMySitePrompts.Companion.getPromptDetailsForTask +import org.wordpress.android.ui.quickstart.QuickStartMySitePrompts.Companion.isTargetingBottomNavBar +import org.wordpress.android.ui.quickstart.QuickStartNoticeDetails +import org.wordpress.android.ui.themes.ThemeBrowserActivity +import org.wordpress.android.ui.uploads.UploadService +import org.wordpress.android.ui.uploads.UploadService.UploadErrorEvent +import org.wordpress.android.ui.uploads.UploadService.UploadMediaSuccessEvent +import org.wordpress.android.ui.uploads.UploadUtilsWrapper +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T.DOMAIN_REGISTRATION +import org.wordpress.android.util.AppLog.T.EDITOR +import org.wordpress.android.util.AppLog.T.MAIN +import org.wordpress.android.util.AppLog.T.UTILS +import org.wordpress.android.util.DateTimeUtils +import org.wordpress.android.util.DisplayUtils +import org.wordpress.android.util.FluxCUtils +import org.wordpress.android.util.MediaUtils +import org.wordpress.android.util.PhotonUtils +import org.wordpress.android.util.PhotonUtils.Quality.HIGH +import org.wordpress.android.util.QuickStartUtils.Companion.addQuickStartFocusPointAboveTheView +import org.wordpress.android.util.QuickStartUtils.Companion.completeTaskAndRemindNextOne +import org.wordpress.android.util.QuickStartUtils.Companion.getNextUncompletedQuickStartTask +import org.wordpress.android.util.QuickStartUtils.Companion.isQuickStartInProgress +import org.wordpress.android.util.QuickStartUtils.Companion.removeQuickStartFocusPoint +import org.wordpress.android.util.QuickStartUtils.Companion.stylizeQuickStartPrompt +import org.wordpress.android.util.SiteUtils +import org.wordpress.android.util.ToastUtils +import org.wordpress.android.util.ToastUtils.Duration.SHORT +import org.wordpress.android.util.WPMediaUtils +import org.wordpress.android.util.analytics.AnalyticsUtils +import org.wordpress.android.util.image.ImageManager +import org.wordpress.android.util.image.ImageType.BLAVATAR +import org.wordpress.android.util.image.ImageType.USER +import org.wordpress.android.util.requestEmailValidation +import org.wordpress.android.widgets.WPDialogSnackbar +import java.io.File +import java.util.GregorianCalendar +import java.util.TimeZone +import javax.inject.Inject + +class MySiteFragment : Fragment(), + SiteSettingsListener, + OnScrollToTopListener, + BasicDialogPositiveClickInterface, + BasicDialogNegativeClickInterface, + BasicDialogOnDismissByOutsideTouchInterface, + PromoDialogClickInterface, + OnConfirmListener, + OnDismissListener { + private var siteSettings: SiteSettingsInterface? = null + private var activeTutorialPrompt: QuickStartMySitePrompts? = null + private val quickStartSnackBarHandler = Handler() + private var blavatarSz = 0 + private var isDomainCreditAvailable = false + private var isDomainCreditChecked = false + + @Inject lateinit var accountStore: AccountStore + @Inject lateinit var dispatcher: Dispatcher + @Inject lateinit var mediaStore: MediaStore + @Inject lateinit var quickStartStore: QuickStartStore + @Inject lateinit var imageManager: ImageManager + @Inject lateinit var uploadUtilsWrapper: UploadUtilsWrapper + @Inject lateinit var meGravatarLoader: MeGravatarLoader + val selectedSite: SiteModel? + get() { + return (activity as? WPMainActivity)?.selectedSite + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + if (savedInstanceState != null) { + activeTutorialPrompt = savedInstanceState + .getSerializable(QuickStartMySitePrompts.KEY) as? QuickStartMySitePrompts + isDomainCreditAvailable = savedInstanceState.getBoolean( + KEY_IS_DOMAIN_CREDIT_AVAILABLE, + false + ) + isDomainCreditChecked = savedInstanceState.getBoolean( + KEY_DOMAIN_CREDIT_CHECKED, + false + ) + } + } + + override fun onDestroy() { + siteSettings?.clear() + super.onDestroy() + } + + private fun refreshMeGravatar() { + val avatarUrl = meGravatarLoader.constructGravatarUrl(accountStore.account.avatarUrl) + meGravatarLoader.load( + false, + avatarUrl, + null, + avatar, + USER, + null + ) + } + + override fun onResume() { + super.onResume() + updateSiteSettingsIfNecessary() + + // Site details may have changed (e.g. via Settings and returning to this Fragment) so update the UI + refreshSelectedSiteDetails(selectedSite) + refreshMeGravatar() + selectedSite?.let { site -> + val isNotAdmin = !site.hasCapabilityManageOptions + val isSelfHostedWithoutJetpack = !SiteUtils.isAccessedViaWPComRest( + site + ) && !site.isJetpackConnected + if (isNotAdmin || isSelfHostedWithoutJetpack) { + row_activity_log.visibility = View.GONE + } else { + row_activity_log.visibility = View.VISIBLE + } + } + updateQuickStartContainer() + if (!AppPrefs.hasQuickStartMigrationDialogShown() && isQuickStartInProgress(quickStartStore)) { + showQuickStartDialogMigration() + } + showQuickStartNoticeIfNecessary() + } + + private fun showQuickStartNoticeIfNecessary() { + if (!isQuickStartInProgress(quickStartStore) || !AppPrefs.isQuickStartNoticeRequired()) { + return + } + val taskToPrompt = getNextUncompletedQuickStartTask( + quickStartStore, + AppPrefs.getSelectedSite().toLong(), CUSTOMIZE + ) // CUSTOMIZE is default type + if (taskToPrompt != null) { + quickStartSnackBarHandler.removeCallbacksAndMessages(null) + quickStartSnackBarHandler.postDelayed({ + if (!isAdded || view == null || activity !is WPMainActivity) { + return@postDelayed + } + val noticeDetails = QuickStartNoticeDetails.getNoticeForTask(taskToPrompt) ?: return@postDelayed + val noticeTitle = getString(noticeDetails.titleResId) + val noticeMessage = getString(noticeDetails.messageResId) + val quickStartNoticeSnackBar = WPDialogSnackbar.make( + requireActivity().findViewById(R.id.coordinator), + noticeMessage, + resources.getInteger(R.integer.quick_start_snackbar_duration_ms) + ) + quickStartNoticeSnackBar.setTitle(noticeTitle) + quickStartNoticeSnackBar.setPositiveButton( + getString(R.string.quick_start_button_positive) + ) { + AnalyticsTracker.track(QUICK_START_TASK_DIALOG_POSITIVE_TAPPED) + activeTutorialPrompt = getPromptDetailsForTask(taskToPrompt) + showActiveQuickStartTutorial() + } + quickStartNoticeSnackBar + .setNegativeButton( + getString(R.string.quick_start_button_negative) + ) { + AnalyticsTracker.track( + QUICK_START_TASK_DIALOG_NEGATIVE_TAPPED + ) + } + (requireActivity() as WPMainActivity).showQuickStartSnackBar(quickStartNoticeSnackBar) + AnalyticsTracker.track(QUICK_START_TASK_DIALOG_VIEWED) + AppPrefs.setQuickStartNoticeRequired(false) + }, AUTO_QUICK_START_SNACKBAR_DELAY_MS.toLong()) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putSerializable(QuickStartMySitePrompts.KEY, activeTutorialPrompt) + outState.putBoolean(KEY_IS_DOMAIN_CREDIT_AVAILABLE, isDomainCreditAvailable) + outState.putBoolean(KEY_DOMAIN_CREDIT_CHECKED, isDomainCreditChecked) + } + + private fun updateSiteSettingsIfNecessary() { + // If the selected site is null, we can't update its site settings + val selectedSite = selectedSite ?: return + if (siteSettings != null && siteSettings!!.localSiteId != selectedSite.id) { + // The site has changed, we can't use the previous site settings, force a refresh + siteSettings = null + } + if (siteSettings == null) { + siteSettings = SiteSettingsInterface.getInterface(activity, selectedSite, this) + if (siteSettings != null) { + siteSettings!!.init(true) + } + } + } + + override fun onPause() { + super.onPause() + clearActiveQuickStart() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val rootView = inflater.inflate(R.layout.my_site_fragment, container, false) as ViewGroup + blavatarSz = resources.getDimensionPixelSize(R.dimen.blavatar_sz_small) + return rootView + } + + private fun setupClickListeners() { + site_info_container.setOnClickListener { viewSite() } + switch_site.setOnClickListener { showSitePicker() } + row_view_site.setOnClickListener { viewSite() } + my_site_register_domain_cta.setOnClickListener { registerDomain() } + quick_action_stats_button.setOnClickListener { + AnalyticsTracker.track(QUICK_ACTION_STATS_TAPPED) + viewStats() + } + row_stats.setOnClickListener { viewStats() } + my_site_blavatar.setOnClickListener { updateBlavatar() } + row_plan.setOnClickListener { + completeQuickStarTask(EXPLORE_PLANS) + ActivityLauncher.viewBlogPlans(activity, selectedSite) + } + quick_action_posts_button.setOnClickListener { + AnalyticsTracker.track(QUICK_ACTION_POSTS_TAPPED) + viewPosts() + } + row_blog_posts.setOnClickListener { viewPosts() } + quick_action_media_button.setOnClickListener { + AnalyticsTracker.track(QUICK_ACTION_MEDIA_TAPPED) + viewMedia() + } + row_media.setOnClickListener { viewMedia() } + quick_action_pages_button.setOnClickListener { + AnalyticsTracker.track(QUICK_ACTION_PAGES_TAPPED) + viewPages() + } + row_pages.setOnClickListener { viewPages() } + row_comments.setOnClickListener { + ActivityLauncher.viewCurrentBlogComments( + activity, + selectedSite + ) + } + row_themes.setOnClickListener { + completeQuickStarTask(CHOOSE_THEME) + if (isQuickStartTaskActive(CUSTOMIZE_SITE)) { + requestNextStepOfActiveQuickStartTask() + } + ActivityLauncher.viewCurrentBlogThemes(activity, selectedSite) + } + row_people.setOnClickListener { + ActivityLauncher.viewCurrentBlogPeople( + activity, + selectedSite + ) + } + row_plugins.setOnClickListener { + ActivityLauncher.viewPluginBrowser( + activity, + selectedSite + ) + } + row_activity_log.setOnClickListener { + ActivityLauncher.viewActivityLogList( + activity, + selectedSite + ) + } + row_settings.setOnClickListener { + ActivityLauncher.viewBlogSettingsForResult( + activity, + selectedSite + ) + } + row_sharing.setOnClickListener { + if (isQuickStartTaskActive(ENABLE_POST_SHARING)) { + requestNextStepOfActiveQuickStartTask() + } + ActivityLauncher.viewBlogSharing(activity, selectedSite) + } + row_admin.setOnClickListener { + ActivityLauncher.viewBlogAdmin( + activity, + selectedSite + ) + } + actionable_empty_view.button.setOnClickListener { + SitePickerActivity.addSite( + activity, + accountStore.hasAccessToken() + ) + } + quick_start_customize.setOnClickListener { + showQuickStartList( + CUSTOMIZE + ) + } + quick_start_grow.setOnClickListener { + showQuickStartList( + GROW + ) + } + quick_start_more.setOnClickListener { showQuickStartCardMenu() } + } + + private fun registerDomain() { + AnalyticsUtils.trackWithSiteDetails(DOMAIN_CREDIT_REDEMPTION_TAPPED, selectedSite) + ActivityLauncher.viewDomainRegistrationActivityForResult( + activity, selectedSite, + CTA_DOMAIN_CREDIT_REDEMPTION + ) + } + + private fun viewMedia() { + ActivityLauncher.viewCurrentBlogMedia(activity, selectedSite) + } + + private fun updateBlavatar() { + AnalyticsTracker.track(MY_SITE_ICON_TAPPED) + val site = selectedSite + if (site != null) { + val hasIcon = site.iconUrl != null + if (site.hasCapabilityManageOptions && site.hasCapabilityUploadFiles) { + if (hasIcon) { + showChangeSiteIconDialog() + } else { + showAddSiteIconDialog() + } + completeQuickStarTask(UPLOAD_SITE_ICON) + } else { + val message = if (hasIcon) { + R.string.my_site_icon_dialog_change_requires_permission_message + } else { + R.string.my_site_icon_dialog_add_requires_permission_message + } + showEditingSiteIconRequiresPermissionDialog(getString(message)) + } + } + } + + private fun viewPosts() { + requestNextStepOfActiveQuickStartTask() + val selectedSite = selectedSite + if (selectedSite != null) { + ActivityLauncher.viewCurrentBlogPosts(requireActivity(), selectedSite) + } else { + ToastUtils.showToast(activity, R.string.site_cannot_be_loaded) + } + } + + private fun viewPages() { + requestNextStepOfActiveQuickStartTask() + val selectedSite = selectedSite + if (selectedSite != null) { + ActivityLauncher.viewCurrentBlogPages(requireActivity(), selectedSite) + } else { + ToastUtils.showToast(activity, R.string.site_cannot_be_loaded) + } + } + + private fun viewStats() { + val selectedSite = selectedSite + if (selectedSite != null) { + completeQuickStarTask(CHECK_STATS) + if (!accountStore.hasAccessToken() && selectedSite.isJetpackConnected) { + // If the user is not connected to WordPress.com, ask him to connect first. + startWPComLoginForJetpackStats() + } else if (selectedSite.isWPCom || selectedSite.isJetpackInstalled && selectedSite + .isJetpackConnected) { + ActivityLauncher.viewBlogStats(activity, selectedSite) + } else { + ActivityLauncher.viewConnectJetpackForStats(activity, selectedSite) + } + } + } + + private fun viewSite() { + completeQuickStarTask(VIEW_SITE) + ActivityLauncher.viewCurrentSite(activity, selectedSite, true) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupClickListeners() + toolbar_main.setTitle(R.string.my_site_section_screen_title) + toolbar_main.inflateMenu(R.menu.my_site_menu) + val meMenu = toolbar_main.menu.findItem(R.id.me_item) + val actionView = meMenu.actionView + actionView.setOnClickListener { + ActivityLauncher.viewMeActivityForResult( + activity + ) + } + actionView.let { + TooltipCompat.setTooltipText(it, meMenu.title) + } + if (activeTutorialPrompt != null) { + showQuickStartFocusPoint() + } + } + + private fun updateQuickStartContainer() { + if (!isAdded) { + return + } + if (isQuickStartInProgress(quickStartStore)) { + val site = AppPrefs.getSelectedSite() + val countCustomizeCompleted = quickStartStore.getCompletedTasksByType( + site.toLong(), + CUSTOMIZE + ).size + val countCustomizeUncompleted = quickStartStore.getUncompletedTasksByType( + site.toLong(), + CUSTOMIZE + ).size + val countGrowCompleted = quickStartStore.getCompletedTasksByType( + site.toLong(), + GROW + ).size + val countGrowUncompleted = quickStartStore.getUncompletedTasksByType( + site.toLong(), + GROW + ).size + if (countCustomizeUncompleted > 0) { + quick_start_customize_icon.isEnabled = true + quick_start_customize_title.isEnabled = true + val updatedPaintFlags = quick_start_customize_title.paintFlags and STRIKE_THRU_TEXT_FLAG.inv() + quick_start_customize_title.paintFlags = updatedPaintFlags + } else { + quick_start_customize_icon.isEnabled = false + quick_start_customize_title.isEnabled = false + quick_start_customize_title.paintFlags = quick_start_customize_title.paintFlags or STRIKE_THRU_TEXT_FLAG + } + quick_start_customize_subtitle.text = getString( + R.string.quick_start_sites_type_subtitle, + countCustomizeCompleted, countCustomizeCompleted + countCustomizeUncompleted + ) + if (countGrowUncompleted > 0) { + quick_start_grow_icon.setBackgroundResource(R.drawable.bg_oval_pink_50_multiple_users_white_40dp) + quick_start_grow_title.isEnabled = true + quick_start_grow_title.paintFlags = quick_start_grow_title.paintFlags and STRIKE_THRU_TEXT_FLAG.inv() + } else { + quick_start_grow_icon.setBackgroundResource(R.drawable.bg_oval_neutral_30_multiple_users_white_40dp) + quick_start_grow_title.isEnabled = false + quick_start_grow_title.paintFlags = quick_start_grow_title.paintFlags or STRIKE_THRU_TEXT_FLAG + } + quick_start_grow_subtitle.text = getString( + R.string.quick_start_sites_type_subtitle, + countGrowCompleted, countGrowCompleted + countGrowUncompleted + ) + quick_start.visibility = View.VISIBLE + } else { + quick_start.visibility = View.GONE + } + } + + private fun showQuickStartCardMenu() { + val quickStartPopupMenu = PopupMenu( + requireContext(), + quick_start_more + ) + quickStartPopupMenu.setOnMenuItemClickListener { item: MenuItem -> + if (item.itemId == R.id.quick_start_card_menu_remove) { + showRemoveNextStepsDialog() + return@setOnMenuItemClickListener true + } + false + } + quickStartPopupMenu.inflate(R.menu.quick_start_card_menu) + quickStartPopupMenu.show() + } + + private fun showQuickStartList(type: QuickStartTaskType) { + clearActiveQuickStart() + val bundle = QuickStartFullScreenDialogFragment.newBundle(type) + when (type) { + CUSTOMIZE -> Builder(requireContext()) + .setTitle(R.string.quick_start_sites_type_customize) + .setOnConfirmListener(this) + .setOnDismissListener(this) + .setContent(QuickStartFullScreenDialogFragment::class.java, bundle) + .build() + .show(requireActivity().supportFragmentManager, FullScreenDialogFragment.TAG) + GROW -> Builder(requireContext()) + .setTitle(R.string.quick_start_sites_type_grow) + .setOnConfirmListener(this) + .setOnDismissListener(this) + .setContent(QuickStartFullScreenDialogFragment::class.java, bundle) + .build() + .show(requireActivity().supportFragmentManager, FullScreenDialogFragment.TAG) + UNKNOWN -> { + } + } + } + + private fun showAddSiteIconDialog() { + val dialog = BasicFragmentDialog() + val tag = TAG_ADD_SITE_ICON_DIALOG + dialog.initialize( + tag, getString(R.string.my_site_icon_dialog_title), + getString(R.string.my_site_icon_dialog_add_message), + getString(R.string.yes), + getString(R.string.no), + null + ) + dialog.show(requireActivity().supportFragmentManager, tag) + } + + private fun showChangeSiteIconDialog() { + val dialog = BasicFragmentDialog() + val tag = TAG_CHANGE_SITE_ICON_DIALOG + dialog.initialize( + tag, getString(R.string.my_site_icon_dialog_title), + getString(R.string.my_site_icon_dialog_change_message), + getString(R.string.my_site_icon_dialog_change_button), + getString(R.string.my_site_icon_dialog_remove_button), + getString(R.string.my_site_icon_dialog_cancel_button) + ) + dialog.show(requireActivity().supportFragmentManager, tag) + } + + private fun showEditingSiteIconRequiresPermissionDialog(message: String) { + val dialog = BasicFragmentDialog() + val tag = TAG_EDIT_SITE_ICON_PERMISSIONS_DIALOG + dialog.initialize( + tag, getString(R.string.my_site_icon_dialog_title), + message, + getString(R.string.dialog_button_ok), + null, + null + ) + dialog.show(requireActivity().supportFragmentManager, tag) + } + + private fun showRemoveNextStepsDialog() { + val dialog = BasicFragmentDialog() + val tag = TAG_REMOVE_NEXT_STEPS_DIALOG + dialog.initialize( + tag, getString(R.string.quick_start_dialog_remove_next_steps_title), + getString(R.string.quick_start_dialog_remove_next_steps_message), + getString(R.string.remove), + getString(R.string.cancel), + null + ) + dialog.show(requireActivity().supportFragmentManager, tag) + } + + private fun startWPComLoginForJetpackStats() { + val loginIntent = Intent(activity, LoginActivity::class.java) + JETPACK_STATS.putInto(loginIntent) + startActivityForResult(loginIntent, RequestCodes.DO_LOGIN) + } + + private fun showSitePicker() { + if (isAdded) { + ActivityLauncher.showSitePickerForResult(activity, selectedSite) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + RequestCodes.DO_LOGIN -> if (resultCode == Activity.RESULT_OK) { + ActivityLauncher.viewBlogStats(activity, selectedSite) + } + RequestCodes.SITE_PICKER -> if (resultCode == Activity.RESULT_OK) { + // reset comments status filter + AppPrefs.setCommentsStatusFilter(ALL) + // reset domain credit flag - it will be checked in onSiteChanged + isDomainCreditAvailable = false + } + RequestCodes.PHOTO_PICKER -> if (resultCode == Activity.RESULT_OK && data != null) { + if (data.hasExtra(PhotoPickerActivity.EXTRA_MEDIA_ID)) { + val mediaId = data.getLongExtra(PhotoPickerActivity.EXTRA_MEDIA_ID, 0).toInt() + showSiteIconProgressBar(true) + updateSiteIconMediaId(mediaId) + } else { + val mediaUriStringsArray = data.getStringArrayExtra( + PhotoPickerActivity.EXTRA_MEDIA_URIS + ) + if (mediaUriStringsArray.isNullOrEmpty()) { + AppLog.e( + UTILS, + "Can't resolve picked or captured image" + ) + return + } + val source = PhotoPickerMediaSource.fromString( + data.getStringExtra(PhotoPickerActivity.EXTRA_MEDIA_SOURCE) + ) + val stat = if (source == ANDROID_CAMERA) MY_SITE_ICON_SHOT_NEW else MY_SITE_ICON_GALLERY_PICKED + AnalyticsTracker.track(stat) + val imageUri = Uri.parse(mediaUriStringsArray[0]) + if (imageUri != null) { + val didGoWell = WPMediaUtils.fetchMediaAndDoNext( + activity, imageUri + ) { uri: Uri -> + showSiteIconProgressBar(true) + startCropActivity(uri) + } + if (!didGoWell) { + AppLog.e( + UTILS, + "Can't download picked or captured image" + ) + } + } + } + } + UCrop.REQUEST_CROP -> if (resultCode == Activity.RESULT_OK) { + AnalyticsTracker.track(MY_SITE_ICON_CROPPED) + WPMediaUtils.fetchMediaAndDoNext( + activity, UCrop.getOutput(data!!) + ) { uri: Uri? -> + startSiteIconUpload( + MediaUtils.getRealPathFromURI(activity, uri) + ) + } + } else if (resultCode == UCrop.RESULT_ERROR) { + AppLog.e( + MAIN, + "Image cropping failed!", + UCrop.getError(data!!) + ) + ToastUtils.showToast( + activity, + R.string.error_cropping_image, + SHORT + ) + } + RequestCodes.DOMAIN_REGISTRATION -> if (resultCode == Activity.RESULT_OK && isAdded && data != null) { + AnalyticsTracker.track(DOMAIN_CREDIT_REDEMPTION_SUCCESS) + val email = data.getStringExtra(DomainRegistrationResultFragment.RESULT_REGISTERED_DOMAIN_EMAIL) + requestEmailValidation(requireContext(), email) + } + } + } + + override fun onConfirm(result: Bundle?) { + if (result != null) { + val task = result.getSerializable(QuickStartFullScreenDialogFragment.RESULT_TASK) as? QuickStartTask + if (task == null || task == CREATE_SITE) { + return + } + + // Remove existing quick start indicator, if necessary. + if (activeTutorialPrompt != null) { + removeQuickStartFocusPoint() + } + activeTutorialPrompt = getPromptDetailsForTask(task) + showActiveQuickStartTutorial() + } + } + + override fun onDismiss() { + updateQuickStartContainer() + } + + private fun startSiteIconUpload(filePath: String) { + if (TextUtils.isEmpty(filePath)) { + ToastUtils.showToast( + activity, + R.string.error_locating_image, + SHORT + ) + return + } + val file = File(filePath) + if (!file.exists()) { + ToastUtils.showToast( + activity, + R.string.file_error_create, + SHORT + ) + return + } + val site = selectedSite + if (site != null) { + val media = buildMediaModel(file, site) + if (media == null) { + ToastUtils.showToast( + activity, + R.string.file_not_found, + SHORT + ) + return + } + UploadService.uploadMedia(activity, media) + } else { + ToastUtils.showToast( + activity, + R.string.error_generic, + SHORT + ) + AppLog.e( + MAIN, + "Unexpected error - Site icon upload failed, because there wasn't any site selected." + ) + } + } + + private fun showSiteIconProgressBar(isVisible: Boolean) { + if (my_site_icon_progress != null && my_site_blavatar != null) { + if (isVisible) { + my_site_icon_progress.visibility = View.VISIBLE + my_site_blavatar.visibility = View.INVISIBLE + } else { + my_site_icon_progress.visibility = View.GONE + my_site_blavatar.visibility = View.VISIBLE + } + } + } + + private val isMediaUploadInProgress: Boolean + get() = my_site_icon_progress.visibility == View.VISIBLE + + private fun buildMediaModel(file: File, site: SiteModel): MediaModel? { + val uri = Uri.Builder().path(file.path).build() + val mimeType = requireActivity().contentResolver.getType(uri) + return FluxCUtils.mediaModelFromLocalUri(requireActivity(), uri, mimeType, mediaStore, site.id) + } + + private fun startCropActivity(uri: Uri) { + val context = activity ?: return + val options = Options() + options.setShowCropGrid(false) + options.setStatusBarColor(ContextCompat.getColor(context, R.color.status_bar)) + options.setToolbarColor(ContextCompat.getColor(context, R.color.primary)) + options.setAllowedGestures(UCropActivity.SCALE, UCropActivity.NONE, UCropActivity.NONE) + options.setHideBottomControls(true) + UCrop.of(uri, Uri.fromFile(File(context.cacheDir, "cropped_for_site_icon.jpg"))) + .withAspectRatio(1f, 1f) + .withOptions(options) + .start(requireActivity(), this) + } + + private fun refreshSelectedSiteDetails(site: SiteModel?) { + if (!isAdded || view == null) { + return + } + if (site == null) { + scroll_view.visibility = View.GONE + actionable_empty_view.visibility = View.VISIBLE + + // Hide actionable empty view image when screen height is under 600 pixels. + if (DisplayUtils.getDisplayPixelHeight(activity) >= 600) { + actionable_empty_view.image.visibility = View.VISIBLE + } else { + actionable_empty_view.image.visibility = View.GONE + } + return + } + if (SiteUtils.onFreePlan(site) || SiteUtils.hasCustomDomain( + site + )) { + isDomainCreditAvailable = false + toggleDomainRegistrationCtaVisibility() + } else if (!isDomainCreditChecked) { + fetchSitePlans(site) + } else { + toggleDomainRegistrationCtaVisibility() + } + scroll_view.visibility = View.VISIBLE + actionable_empty_view.visibility = View.GONE + toggleAdminVisibility(site) + val themesVisibility = if (ThemeBrowserActivity.isAccessible(site)) View.VISIBLE else View.GONE + my_site_look_and_feel_header.visibility = themesVisibility + row_themes.visibility = themesVisibility + + // sharing is only exposed for sites accessed via the WPCOM REST API (wpcom or Jetpack) + val sharingVisibility = if (SiteUtils.isAccessedViaWPComRest(site)) View.VISIBLE else View.GONE + row_sharing.visibility = sharingVisibility + + // show settings for all self-hosted to expose Delete Site + val isAdminOrSelfHosted = site.hasCapabilityManageOptions || !SiteUtils.isAccessedViaWPComRest( + site + ) + row_settings.visibility = if (isAdminOrSelfHosted) View.VISIBLE else View.GONE + row_people.visibility = if (site.hasCapabilityListUsers) View.VISIBLE else View.GONE + row_plugins.visibility = if (PluginUtils.isPluginFeatureAvailable(site)) View.VISIBLE else View.GONE + + // if either people or settings is visible, configuration header should be visible + val settingsVisibility = if (isAdminOrSelfHosted || site.hasCapabilityListUsers) View.VISIBLE else View.GONE + my_site_configuration_header.visibility = settingsVisibility + imageManager.load( + my_site_blavatar, + BLAVATAR, + SiteUtils.getSiteIconUrl(site, blavatarSz) + ) + val homeUrl = SiteUtils.getHomeURLOrHostName(site) + val blogTitle = SiteUtils.getSiteNameOrHomeURL(site) + my_site_title_label.text = blogTitle + my_site_subtitle_label.text = homeUrl + + // Hide the Plan item if the Plans feature is not available for this blog + val planShortName = site.planShortName + if (!TextUtils.isEmpty(planShortName) && site.hasCapabilityManageOptions) { + if (site.isWPCom || site.isAutomatedTransfer) { + my_site_current_plan_text_view.text = planShortName + row_plan.visibility = View.VISIBLE + } else { + // TODO: Support Jetpack plans + row_plan.visibility = View.GONE + } + } else { + row_plan.visibility = View.GONE + } + + // Do not show pages menu item to Collaborators. + val pageVisibility = if (site.isSelfHostedAdmin || site.hasCapabilityEditPages) View.VISIBLE else View.GONE + row_pages.visibility = pageVisibility + quick_action_pages_container.visibility = pageVisibility + if (pageVisibility == View.VISIBLE) { + quick_action_buttons_container.weightSum = 100f + } else { + quick_action_buttons_container.weightSum = 75f + } + } + + private fun toggleAdminVisibility(site: SiteModel?) { + if (site == null) { + return + } + if (shouldHideWPAdmin(site)) { + row_admin.visibility = View.GONE + } else { + row_admin.visibility = View.VISIBLE + } + } + + private fun shouldHideWPAdmin(site: SiteModel?): Boolean { + if (site == null) { + return false + } + return if (!site.isWPCom) { + false + } else { + val dateCreated = DateTimeUtils.dateFromIso8601( + accountStore.account + .date + ) + val calendar = GregorianCalendar( + HIDE_WP_ADMIN_YEAR, HIDE_WP_ADMIN_MONTH, + HIDE_WP_ADMIN_DAY + ) + calendar.timeZone = TimeZone.getTimeZone(HIDE_WP_ADMIN_GMT_TIME_ZONE) + dateCreated != null && dateCreated.after(calendar.time) + } + } + + override fun onScrollToTop() { + if (isAdded) { + scroll_view.smoothScrollTo(0, 0) + } + } + + override fun onStop() { + dispatcher.unregister(this) + EventBus.getDefault().unregister(this) + super.onStop() + } + + override fun onStart() { + super.onStart() + dispatcher.register(this) + EventBus.getDefault().register(this) + } + + /** + * We can't just use fluxc OnSiteChanged event, as the order of events is not guaranteed -> getSelectedSite() + * method might return an out of date SiteModel, if the OnSiteChanged event handler in the WPMainActivity wasn't + * called yet. + */ + fun onSiteChanged(site: SiteModel?) { + // whenever site changes we hide CTA and check for credit in refreshSelectedSiteDetails() + isDomainCreditChecked = false + refreshSelectedSiteDetails(site) + showSiteIconProgressBar(false) + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEventMainThread(event: UploadErrorEvent) { + AnalyticsTracker.track(MY_SITE_ICON_UPLOAD_UNSUCCESSFUL) + EventBus.getDefault().removeStickyEvent(event) + if (isMediaUploadInProgress) { + showSiteIconProgressBar(false) + } + val site = selectedSite + if (site != null && event.post != null) { + if (event.post.localSiteId == site.id) { + uploadUtilsWrapper.onPostUploadedSnackbarHandler( + activity, + requireActivity().findViewById(R.id.coordinator), true, + event.post, event.errorMessage, site + ) + } + } else if (event.mediaModelList != null && event.mediaModelList.isNotEmpty()) { + uploadUtilsWrapper.onMediaUploadedSnackbarHandler( + activity, + requireActivity().findViewById(R.id.coordinator), true, + event.mediaModelList, site, event.errorMessage + ) + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEventMainThread(event: UploadMediaSuccessEvent) { + AnalyticsTracker.track(MY_SITE_ICON_UPLOADED) + EventBus.getDefault().removeStickyEvent(event) + val site = selectedSite + if (site != null) { + if (isMediaUploadInProgress) { + if (event.mediaModelList.size > 0) { + val media = event.mediaModelList[0] + imageManager.load( + my_site_blavatar, + BLAVATAR, + PhotonUtils + .getPhotonImageUrl( + media.url, + blavatarSz, + blavatarSz, + HIGH, + site.isPrivateWPComAtomic + ) + ) + updateSiteIconMediaId(media.mediaId.toInt()) + } else { + AppLog.w( + MAIN, + "Site icon upload completed, but mediaList is empty." + ) + } + showSiteIconProgressBar(false) + } else { + if (event.mediaModelList != null && event.mediaModelList.isNotEmpty()) { + uploadUtilsWrapper.onMediaUploadedSnackbarHandler( + activity, + requireActivity().findViewById(R.id.coordinator), false, + event.mediaModelList, site, event.successMessage + ) + } + } + } + } + + override fun onPositiveClicked(instanceTag: String) { + when (instanceTag) { + TAG_ADD_SITE_ICON_DIALOG, TAG_CHANGE_SITE_ICON_DIALOG -> ActivityLauncher.showPhotoPickerForResult( + activity, + SITE_ICON_PICKER, selectedSite, null + ) + TAG_EDIT_SITE_ICON_PERMISSIONS_DIALOG -> { + } + TAG_QUICK_START_DIALOG -> { + startQuickStart() + AnalyticsTracker.track(QUICK_START_REQUEST_DIALOG_POSITIVE_TAPPED) + } + TAG_QUICK_START_MIGRATION_DIALOG -> AnalyticsTracker.track(QUICK_START_MIGRATION_DIALOG_POSITIVE_TAPPED) + TAG_REMOVE_NEXT_STEPS_DIALOG -> { + AnalyticsTracker.track(QUICK_START_REMOVE_DIALOG_POSITIVE_TAPPED) + skipQuickStart() + updateQuickStartContainer() + clearActiveQuickStart() + } + else -> { + AppLog.e( + EDITOR, + "Dialog instanceTag is not recognized" + ) + throw UnsupportedOperationException("Dialog instanceTag is not recognized") + } + } + } + + private fun skipQuickStart() { + val siteId = AppPrefs.getSelectedSite() + for (quickStartTask in QuickStartTask.values()) { + quickStartStore.setDoneTask(siteId.toLong(), quickStartTask, true) + } + quickStartStore.setQuickStartCompleted(siteId.toLong(), true) + // skipping all tasks means no achievement notification, so we mark it as received + quickStartStore.setQuickStartNotificationReceived(siteId.toLong(), true) + } + + private fun startQuickStart() { + quickStartStore.setDoneTask(AppPrefs.getSelectedSite().toLong(), CREATE_SITE, true) + updateQuickStartContainer() + } + + private fun toggleDomainRegistrationCtaVisibility() { + if (isDomainCreditAvailable) { + // we nest this check because of some weirdness with ui state and race conditions + if (my_site_register_domain_cta.visibility != View.VISIBLE) { + AnalyticsTracker.track(DOMAIN_CREDIT_PROMPT_SHOWN) + my_site_register_domain_cta.visibility = View.VISIBLE + } + } else { + my_site_register_domain_cta.visibility = View.GONE + } + } + + override fun onNegativeClicked(instanceTag: String) { + when (instanceTag) { + TAG_ADD_SITE_ICON_DIALOG -> showQuickStartNoticeIfNecessary() + TAG_CHANGE_SITE_ICON_DIALOG -> { + AnalyticsTracker.track(MY_SITE_ICON_REMOVED) + showSiteIconProgressBar(true) + updateSiteIconMediaId(0) + } + TAG_QUICK_START_DIALOG -> AnalyticsTracker.track(QUICK_START_REQUEST_DIALOG_NEGATIVE_TAPPED) + TAG_REMOVE_NEXT_STEPS_DIALOG -> AnalyticsTracker.track(QUICK_START_REMOVE_DIALOG_NEGATIVE_TAPPED) + else -> { + AppLog.e( + EDITOR, + "Dialog instanceTag '$instanceTag' is not recognized" + ) + throw UnsupportedOperationException("Dialog instanceTag is not recognized") + } + } + } + + override fun onNeutralClicked(instanceTag: String) { + if (TAG_QUICK_START_DIALOG == instanceTag) { + AppPrefs.setQuickStartDisabled(true) + AnalyticsTracker.track(QUICK_START_REQUEST_DIALOG_NEUTRAL_TAPPED) + } else { + AppLog.e( + EDITOR, + "Dialog instanceTag '$instanceTag' is not recognized" + ) + throw UnsupportedOperationException("Dialog instanceTag is not recognized") + } + } + + override fun onDismissByOutsideTouch(instanceTag: String) { + when (instanceTag) { + TAG_ADD_SITE_ICON_DIALOG -> showQuickStartNoticeIfNecessary() + TAG_CHANGE_SITE_ICON_DIALOG, + TAG_EDIT_SITE_ICON_PERMISSIONS_DIALOG, + TAG_QUICK_START_DIALOG, + TAG_QUICK_START_MIGRATION_DIALOG, + TAG_REMOVE_NEXT_STEPS_DIALOG -> { + } + else -> { + AppLog.e( + EDITOR, + "Dialog instanceTag '$instanceTag' is not recognized" + ) + throw UnsupportedOperationException("Dialog instanceTag is not recognized") + } + } + } + + override fun onLinkClicked(instanceTag: String) {} + override fun onSettingsSaved() { + // refresh the site after site icon change + val site = selectedSite + if (site != null) { + dispatcher.dispatch(SiteActionBuilder.newFetchSiteAction(site)) + } + } + + override fun onSaveError(error: Exception) { + showSiteIconProgressBar(false) + } + + override fun onFetchError(error: Exception) { + showSiteIconProgressBar(false) + } + + override fun onSettingsUpdated() {} + override fun onCredentialsValidated(error: Exception?) {} + private fun fetchSitePlans(site: SiteModel?) { + dispatcher.dispatch(SiteActionBuilder.newFetchPlansAction(site)) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onPlansFetched(event: OnPlansFetched) { + if (AppPrefs.getSelectedSite() != event.site.id) { + return + } + if (event.isError) { + AppLog.e( + DOMAIN_REGISTRATION, + "An error occurred while fetching plans : " + event.error.message + ) + } else { + isDomainCreditChecked = true + isDomainCreditAvailable = isDomainCreditAvailable(event.plans) + toggleDomainRegistrationCtaVisibility() + } + } + + private val mAddQuickStartFocusPointTask = Runnable { // technically there is no situation (yet) where fragment is not added but we need to show focus point + if (!isAdded) { + return@Runnable + } + val parentView = requireActivity().findViewById(activeTutorialPrompt!!.parentContainerId) + val quickStartTarget = requireActivity().findViewById( + activeTutorialPrompt!!.focusedContainerId + ) + if (quickStartTarget == null || parentView == null) { + return@Runnable + } + val focusPointSize = resources.getDimensionPixelOffset(R.dimen.quick_start_focus_point_size) + val horizontalOffset: Int + val verticalOffset: Int + if (isTargetingBottomNavBar(activeTutorialPrompt!!.task)) { + horizontalOffset = quickStartTarget.width / 2 - focusPointSize + resources + .getDimensionPixelOffset(R.dimen.quick_start_focus_point_bottom_nav_offset) + verticalOffset = 0 + } else if (activeTutorialPrompt!!.task == UPLOAD_SITE_ICON) { + horizontalOffset = focusPointSize + verticalOffset = -focusPointSize / 2 + } else { + horizontalOffset = resources.getDimensionPixelOffset(R.dimen.quick_start_focus_point_my_site_right_offset) + verticalOffset = (quickStartTarget.height - focusPointSize) / 2 + } + addQuickStartFocusPointAboveTheView( + parentView, quickStartTarget, horizontalOffset, + verticalOffset + ) + + // highlight MySite row and scroll to it + if (!isTargetingBottomNavBar(activeTutorialPrompt!!.task)) { + scroll_view.post { scroll_view.smoothScrollTo(0, quickStartTarget.top) } + } + } + + private fun showQuickStartFocusPoint() { + if (view == null || !hasActiveQuickStartTask()) { + return + } + requireView().post(mAddQuickStartFocusPointTask) + } + + private fun removeQuickStartFocusPoint() { + if (view == null || !isAdded) { + return + } + requireView().removeCallbacks(mAddQuickStartFocusPointTask) + removeQuickStartFocusPoint(requireActivity().findViewById(R.id.root_view_main)) + } + + fun isQuickStartTaskActive(task: QuickStartTask): Boolean { + return hasActiveQuickStartTask() && activeTutorialPrompt!!.task == task + } + + private fun completeQuickStarTask(quickStartTask: QuickStartTask) { + selectedSite?.let { site -> + // we need to process notices for tasks that are completed at MySite fragment + AppPrefs.setQuickStartNoticeRequired( + !quickStartStore.hasDoneTask( + AppPrefs.getSelectedSite().toLong(), quickStartTask + ) && + activeTutorialPrompt != null && + activeTutorialPrompt!!.task == quickStartTask + ) + completeTaskAndRemindNextOne( + quickStartStore, quickStartTask, dispatcher, + site, context = requireContext() + ) + // We update completed tasks counter onResume, but UPLOAD_SITE_ICON can be completed without navigating + // away from the activity, so we are updating counter here + if (quickStartTask == UPLOAD_SITE_ICON) { + updateQuickStartContainer() + } + if (activeTutorialPrompt != null && activeTutorialPrompt!!.task == quickStartTask) { + removeQuickStartFocusPoint() + clearActiveQuickStartTask() + } + } + } + + private fun clearActiveQuickStart() { + // Clear pressed row. + if (activeTutorialPrompt != null && !isTargetingBottomNavBar(activeTutorialPrompt!!.task)) { + requireActivity().findViewById(activeTutorialPrompt!!.focusedContainerId).isPressed = false + } + if (activity != null && !requireActivity().isChangingConfigurations) { + clearActiveQuickStartTask() + removeQuickStartFocusPoint() + } + quickStartSnackBarHandler.removeCallbacksAndMessages(null) + } + + fun requestNextStepOfActiveQuickStartTask() { + if (!hasActiveQuickStartTask()) { + return + } + removeQuickStartFocusPoint() + EventBus.getDefault().postSticky(QuickStartEvent(activeTutorialPrompt!!.task)) + clearActiveQuickStartTask() + } + + private fun clearActiveQuickStartTask() { + activeTutorialPrompt = null + } + + private fun hasActiveQuickStartTask(): Boolean { + return activeTutorialPrompt != null + } + + private fun showActiveQuickStartTutorial() { + if (!hasActiveQuickStartTask() || !isAdded || activity !is WPMainActivity) { + return + } + showQuickStartFocusPoint() + val shortQuickStartMessage = stylizeQuickStartPrompt( + requireActivity(), + activeTutorialPrompt!!.shortMessagePrompt, + activeTutorialPrompt!!.iconId + ) + val promptSnackbar = WPDialogSnackbar.make( + requireActivity().findViewById(R.id.coordinator), + shortQuickStartMessage, resources.getInteger(R.integer.quick_start_snackbar_duration_ms) + ) + (requireActivity() as WPMainActivity).showQuickStartSnackBar(promptSnackbar) + } + + private fun showQuickStartDialogMigration() { + val promoDialog = PromoDialog() + promoDialog.initialize( + TAG_QUICK_START_MIGRATION_DIALOG, + getString(R.string.quick_start_dialog_migration_title), + getString(R.string.quick_start_dialog_migration_message), + getString(android.R.string.ok), + R.drawable.img_illustration_checkmark_280dp, + "", + "", + "" + ) + if (fragmentManager != null) { + promoDialog.show(requireFragmentManager(), TAG_QUICK_START_MIGRATION_DIALOG) + AppPrefs.setQuickStartMigrationDialogShown(true) + AnalyticsTracker.track(QUICK_START_MIGRATION_DIALOG_VIEWED) + } + } + + private fun updateSiteIconMediaId(mediaId: Int) { + siteSettings?.let { + it.setSiteIconMediaId(mediaId) + it.saveSettings() + } + } + + companion object { + const val HIDE_WP_ADMIN_YEAR = 2015 + const val HIDE_WP_ADMIN_MONTH = 9 + const val HIDE_WP_ADMIN_DAY = 7 + const val HIDE_WP_ADMIN_GMT_TIME_ZONE = "GMT" + const val ARG_QUICK_START_TASK = "ARG_QUICK_START_TASK" + const val TAG_ADD_SITE_ICON_DIALOG = "TAG_ADD_SITE_ICON_DIALOG" + const val TAG_REMOVE_NEXT_STEPS_DIALOG = "TAG_REMOVE_NEXT_STEPS_DIALOG" + const val TAG_CHANGE_SITE_ICON_DIALOG = "TAG_CHANGE_SITE_ICON_DIALOG" + const val TAG_EDIT_SITE_ICON_PERMISSIONS_DIALOG = "TAG_EDIT_SITE_ICON_PERMISSIONS_DIALOG" + const val TAG_QUICK_START_DIALOG = "TAG_QUICK_START_DIALOG" + const val TAG_QUICK_START_MIGRATION_DIALOG = "TAG_QUICK_START_MIGRATION_DIALOG" + const val AUTO_QUICK_START_SNACKBAR_DELAY_MS = 1000 + const val KEY_IS_DOMAIN_CREDIT_AVAILABLE = "KEY_IS_DOMAIN_CREDIT_AVAILABLE" + const val KEY_DOMAIN_CREDIT_CHECKED = "KEY_DOMAIN_CREDIT_CHECKED" + fun newInstance(): MySiteFragment { + return MySiteFragment() + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index 9d10f5d07f9f..2e2ec3dd046e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -1426,6 +1426,10 @@ private void hideQuickStartSnackBar() { } } + // The first time this is called in onCreate -> initViewModel we still haven't initialized mSelectedSite, + // which hasFullAccessToContent depends on, and as such the state will be initialized with the most restrictive + // rights case (that is, will assume hasFullAccessToContent is false). This is OK and will be frequently checked + // to normalize the UI state whenever mSelectedSite changes. private boolean hasFullAccessToContent() { return SiteUtils.hasFullAccessToContent(getSelectedSite()); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt index cb6d2606aaf3..f5b0b5374ebb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt @@ -27,6 +27,7 @@ import org.wordpress.android.ui.main.WPMainNavigationView.PageType.READER import org.wordpress.android.ui.notifications.NotificationsListFragment import org.wordpress.android.ui.prefs.AppPrefs import org.wordpress.android.ui.reader.ReaderFragment +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsFragment import org.wordpress.android.util.AniUtils import org.wordpress.android.util.AniUtils.Duration import org.wordpress.android.util.getColorStateListFromAttribute @@ -282,7 +283,11 @@ class WPMainNavigationView @JvmOverloads constructor( private fun createFragment(pageType: PageType): Fragment { val fragment = when (pageType) { MY_SITE -> MySiteFragment.newInstance() - READER -> ReaderFragment() + READER -> if (AppPrefs.isReaderImprovementsPhase2Enabled()) { + ReaderInterestsFragment() // TODO: Temporary entry point + } else { + ReaderFragment() + } NOTIFS -> NotificationsListFragment.newInstance() } fragmentManager.beginTransaction() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java index 11de7e9943ab..d174591e6c57 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java @@ -23,6 +23,7 @@ import org.wordpress.android.fluxc.model.MediaModel; import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState; import org.wordpress.android.fluxc.model.SiteModel; +import org.wordpress.android.ui.utils.AuthenticationUtils; import org.wordpress.android.util.AccessibilityUtils; import org.wordpress.android.util.AniUtils; import org.wordpress.android.util.AppLog; @@ -75,6 +76,7 @@ public class MediaGridAdapter extends RecyclerView.Adapter { if (!isFinishing()) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.java deleted file mode 100644 index a93b610d8d18..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.java +++ /dev/null @@ -1,369 +0,0 @@ -package org.wordpress.android.ui.notifications; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.os.Parcelable; -import android.text.Html; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; - -import com.google.android.material.appbar.AppBarLayout; -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayout.OnTabSelectedListener; -import com.google.android.material.tabs.TabLayout.Tab; - -import org.greenrobot.eventbus.EventBus; -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.fluxc.model.SiteModel; -import org.wordpress.android.fluxc.store.AccountStore; -import org.wordpress.android.ui.ActivityLauncher; -import org.wordpress.android.ui.JetpackConnectionWebViewActivity; -import org.wordpress.android.ui.RequestCodes; -import org.wordpress.android.ui.WPWebViewActivity; -import org.wordpress.android.ui.main.WPMainActivity; -import org.wordpress.android.ui.notifications.adapters.NotesAdapter; -import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS; -import org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.NetworkUtils; -import org.wordpress.android.util.WPUrlUtils; -import org.wordpress.android.widgets.WPViewPager; - -import java.util.HashMap; -import java.util.Map; - -import javax.inject.Inject; - -import static org.wordpress.android.analytics.AnalyticsTracker.NOTIFICATIONS_SELECTED_FILTER; -import static org.wordpress.android.ui.JetpackConnectionSource.NOTIFICATIONS; -import static org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter.IS_TAPPED_ON_NOTIFICATION; -import static org.wordpress.android.ui.stats.StatsConnectJetpackActivity.FAQ_URL; - -public class NotificationsListFragment extends Fragment { - public static final String NOTE_ID_EXTRA = "noteId"; - public static final String NOTE_INSTANT_REPLY_EXTRA = "instantReply"; - public static final String NOTE_PREFILLED_REPLY_EXTRA = "prefilledReplyText"; - public static final String NOTE_MODERATE_ID_EXTRA = "moderateNoteId"; - public static final String NOTE_MODERATE_STATUS_EXTRA = "moderateNoteStatus"; - public static final String NOTE_CURRENT_LIST_FILTER_EXTRA = "currentFilter"; - - protected static final int TAB_COUNT = 5; - protected static final int TAB_POSITION_ALL = 0; - protected static final int TAB_POSITION_UNREAD = 1; - protected static final int TAB_POSITION_COMMENT = 2; - protected static final int TAB_POSITION_FOLLOW = 3; - protected static final int TAB_POSITION_LIKE = 4; - - private static final String KEY_LAST_TAB_POSITION = "lastTabPosition"; - - private TabLayout mTabLayout; - private ViewGroup mConnectJetpackView; - private boolean mShouldRefreshNotifications; - private int mLastTabPosition; - - @Nullable private Toolbar mToolbar = null; - - @Inject AccountStore mAccountStore; - - public static NotificationsListFragment newInstance() { - return new NotificationsListFragment(); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - if (savedInstanceState != null) { - setSelectedTab(savedInstanceState.getInt(KEY_LAST_TAB_POSITION, TAB_POSITION_ALL)); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ((WordPress) requireActivity().getApplication()).component().inject(this); - mShouldRefreshNotifications = true; - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.notifications_list_fragment, container, false); - setHasOptionsMenu(true); - - mConnectJetpackView = view.findViewById(R.id.connect_jetpack); - - mToolbar = view.findViewById(R.id.toolbar_main); - mToolbar.setTitle(R.string.notifications_screen_title); - ((AppCompatActivity) getActivity()).setSupportActionBar(mToolbar); - - mTabLayout = view.findViewById(R.id.tab_layout); - mTabLayout.addOnTabSelectedListener(new OnTabSelectedListener() { - @Override - public void onTabSelected(Tab tab) { - Map properties = new HashMap<>(1); - - switch (tab.getPosition()) { - case TAB_POSITION_ALL: - properties.put(NOTIFICATIONS_SELECTED_FILTER, FILTERS.FILTER_ALL.toString()); - break; - case TAB_POSITION_COMMENT: - properties.put(NOTIFICATIONS_SELECTED_FILTER, FILTERS.FILTER_COMMENT.toString()); - break; - case TAB_POSITION_FOLLOW: - properties.put(NOTIFICATIONS_SELECTED_FILTER, FILTERS.FILTER_FOLLOW.toString()); - break; - case TAB_POSITION_LIKE: - properties.put(NOTIFICATIONS_SELECTED_FILTER, FILTERS.FILTER_LIKE.toString()); - break; - case TAB_POSITION_UNREAD: - properties.put(NOTIFICATIONS_SELECTED_FILTER, FILTERS.FILTER_UNREAD.toString()); - break; - default: - properties.put(NOTIFICATIONS_SELECTED_FILTER, FILTERS.FILTER_ALL.toString()); - break; - } - - AnalyticsTracker.track(Stat.NOTIFICATION_TAPPED_SEGMENTED_CONTROL, properties); - mLastTabPosition = tab.getPosition(); - } - - @Override - public void onTabUnselected(Tab tab) { - } - - @Override - public void onTabReselected(Tab tab) { - } - }); - - WPViewPager viewPager = view.findViewById(R.id.view_pager); - viewPager.setAdapter(new NotificationsFragmentAdapter(getChildFragmentManager())); - viewPager.setPageMargin(getResources().getDimensionPixelSize(R.dimen.margin_extra_large)); - mTabLayout.setupWithViewPager(viewPager); - - TextView jetpackTermsAndConditions = view.findViewById(R.id.jetpack_terms_and_conditions); - jetpackTermsAndConditions.setText(Html.fromHtml(String.format( - getResources().getString(R.string.jetpack_connection_terms_and_conditions), "", ""))); - jetpackTermsAndConditions.setOnClickListener(new OnClickListener() { - @Override public void onClick(View view) { - WPWebViewActivity.openURL(requireContext(), WPUrlUtils.buildTermsOfServiceUrl(getContext())); - } - }); - - Button jetpackFaq = view.findViewById(R.id.jetpack_faq); - jetpackFaq.setOnClickListener(new OnClickListener() { - @Override public void onClick(View view) { - WPWebViewActivity.openURL(requireContext(), FAQ_URL); - } - }); - - return view; - } - - @Override - public void onPause() { - super.onPause(); - mShouldRefreshNotifications = true; - } - - @Override - public void onResume() { - super.onResume(); - EventBus.getDefault().post(new NotificationEvents.NotificationsUnseenStatus(false)); - - if (!mAccountStore.hasAccessToken()) { - showConnectJetpackView(); - mTabLayout.setVisibility(View.GONE); - } else { - if (mShouldRefreshNotifications) { - fetchNotesFromRemote(); - } - } - - setSelectedTab(mLastTabPosition); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putInt(KEY_LAST_TAB_POSITION, mLastTabPosition); - super.onSaveInstanceState(outState); - } - - private void clearToolbarScrollFlags() { - if (mToolbar != null && mToolbar.getLayoutParams() instanceof AppBarLayout.LayoutParams) { - AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams(); - params.setScrollFlags(0); - } - } - - private void fetchNotesFromRemote() { - if (!isAdded()) { - return; - } - - if (!NetworkUtils.isNetworkAvailable(getActivity())) { - return; - } - - NotificationsUpdateServiceStarter.startService(getActivity()); - } - - private static Intent getOpenNoteIntent(Activity activity, String noteId) { - Intent detailIntent = new Intent(activity, NotificationsDetailActivity.class); - detailIntent.putExtra(NOTE_ID_EXTRA, noteId); - return detailIntent; - } - - public SiteModel getSelectedSite() { - if (getActivity() instanceof WPMainActivity) { - WPMainActivity mainActivity = (WPMainActivity) getActivity(); - return mainActivity.getSelectedSite(); - } - - return null; - } - - public static void openNoteForReply(Activity activity, String noteId, boolean shouldShowKeyboard, String replyText, - NotesAdapter.FILTERS filter, boolean isTappedFromPushNotification) { - if (noteId == null || activity == null) { - return; - } - - if (activity.isFinishing()) { - return; - } - - Intent detailIntent = getOpenNoteIntent(activity, noteId); - detailIntent.putExtra(NOTE_INSTANT_REPLY_EXTRA, shouldShowKeyboard); - - if (!TextUtils.isEmpty(replyText)) { - detailIntent.putExtra(NOTE_PREFILLED_REPLY_EXTRA, replyText); - } - - detailIntent.putExtra(NOTE_CURRENT_LIST_FILTER_EXTRA, filter); - detailIntent.putExtra(IS_TAPPED_ON_NOTIFICATION, isTappedFromPushNotification); - openNoteForReplyWithParams(detailIntent, activity); - } - - private static void openNoteForReplyWithParams(Intent detailIntent, Activity activity) { - activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL); - } - - private void setSelectedTab(int position) { - mLastTabPosition = position; - - if (mTabLayout != null) { - TabLayout.Tab tab = mTabLayout.getTabAt(mLastTabPosition); - - if (tab != null) { - tab.select(); - } - } - } - - private void showConnectJetpackView() { - if (isAdded() && mConnectJetpackView != null) { - mConnectJetpackView.setVisibility(View.VISIBLE); - mTabLayout.setVisibility(View.GONE); - clearToolbarScrollFlags(); - - Button setupButton = mConnectJetpackView.findViewById(R.id.jetpack_setup); - setupButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - SiteModel siteModel = getSelectedSite(); - JetpackConnectionWebViewActivity - .startJetpackConnectionFlow(getActivity(), NOTIFICATIONS, siteModel, false); - } - }); - } - } - - private class NotificationsFragmentAdapter extends FragmentPagerAdapter { - NotificationsFragmentAdapter(FragmentManager fragmentManager) { - super(fragmentManager); - } - - @Override - public int getCount() { - return TAB_COUNT; - } - - @Override - public Fragment getItem(int position) { - return NotificationsListFragmentPage.newInstance(position); - } - - @Nullable - @Override - public CharSequence getPageTitle(int position) { - switch (position) { - case TAB_POSITION_ALL: - return getString(R.string.notifications_tab_title_all); - case TAB_POSITION_COMMENT: - return getString(R.string.notifications_tab_title_comments); - case TAB_POSITION_FOLLOW: - return getString(R.string.notifications_tab_title_follows); - case TAB_POSITION_LIKE: - return getString(R.string.notifications_tab_title_likes); - case TAB_POSITION_UNREAD: - return getString(R.string.notifications_tab_title_unread_notifications); - default: - return super.getPageTitle(position); - } - } - - @Override - public void restoreState(Parcelable state, ClassLoader loader) { - try { - super.restoreState(state, loader); - } catch (IllegalStateException exception) { - AppLog.e(T.NOTIFS, exception); - } - } - } - - @Override - public void onPrepareOptionsMenu(Menu menu) { - MenuItem notificationSettings = menu.findItem(R.id.notifications_settings); - notificationSettings.setVisible(mAccountStore.hasAccessToken()); - - super.onPrepareOptionsMenu(menu); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.notifications_list_menu, menu); - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.notifications_settings) { - ActivityLauncher.viewNotificationsSettings(getActivity()); - return true; - } - return super.onOptionsItemSelected(item); - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt new file mode 100644 index 000000000000..49f95b0e7e65 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt @@ -0,0 +1,280 @@ +package org.wordpress.android.ui.notifications + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.text.Html +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import com.google.android.material.appbar.AppBarLayout.LayoutParams +import com.google.android.material.tabs.TabLayout.OnTabSelectedListener +import com.google.android.material.tabs.TabLayout.Tab +import kotlinx.android.synthetic.main.notifications_list_fragment.* +import org.greenrobot.eventbus.EventBus +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.NOTIFICATIONS_SELECTED_FILTER +import org.wordpress.android.analytics.AnalyticsTracker.Stat.NOTIFICATION_TAPPED_SEGMENTED_CONTROL +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.ui.ActivityLauncher +import org.wordpress.android.ui.JetpackConnectionSource.NOTIFICATIONS +import org.wordpress.android.ui.JetpackConnectionWebViewActivity +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.ui.WPWebViewActivity +import org.wordpress.android.ui.main.WPMainActivity +import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsUnseenStatus +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_ALL +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_COMMENT +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_FOLLOW +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_LIKE +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_UNREAD +import org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter +import org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter.IS_TAPPED_ON_NOTIFICATION +import org.wordpress.android.ui.stats.StatsConnectJetpackActivity +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T.NOTIFS +import org.wordpress.android.util.NetworkUtils +import org.wordpress.android.util.WPUrlUtils +import java.util.HashMap +import javax.inject.Inject + +class NotificationsListFragment : Fragment() { + private var shouldRefreshNotifications = false + private var lastTabPosition = 0 + + @Inject lateinit var accountStore: AccountStore + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + if (savedInstanceState != null) { + setSelectedTab(savedInstanceState.getInt(KEY_LAST_TAB_POSITION, TAB_POSITION_ALL)) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + shouldRefreshNotifications = true + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.notifications_list_fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setHasOptionsMenu(true) + toolbar_main.setTitle(R.string.notifications_screen_title) + (requireActivity() as AppCompatActivity).setSupportActionBar(toolbar_main) + + tab_layout.addOnTabSelectedListener(object : OnTabSelectedListener { + override fun onTabSelected(tab: Tab) { + val properties: MutableMap = HashMap(1) + when (tab.position) { + TAB_POSITION_ALL -> properties[NOTIFICATIONS_SELECTED_FILTER] = FILTER_ALL.toString() + TAB_POSITION_COMMENT -> properties[NOTIFICATIONS_SELECTED_FILTER] = FILTER_COMMENT.toString() + TAB_POSITION_FOLLOW -> properties[NOTIFICATIONS_SELECTED_FILTER] = FILTER_FOLLOW.toString() + TAB_POSITION_LIKE -> properties[NOTIFICATIONS_SELECTED_FILTER] = FILTER_LIKE.toString() + TAB_POSITION_UNREAD -> properties[NOTIFICATIONS_SELECTED_FILTER] = FILTER_UNREAD.toString() + else -> properties[NOTIFICATIONS_SELECTED_FILTER] = FILTER_ALL.toString() + } + AnalyticsTracker.track(NOTIFICATION_TAPPED_SEGMENTED_CONTROL, properties) + lastTabPosition = tab.position + } + + override fun onTabUnselected(tab: Tab) {} + override fun onTabReselected(tab: Tab) {} + }) + view_pager.adapter = NotificationsFragmentAdapter(childFragmentManager, buildTitles()) + view_pager.pageMargin = resources.getDimensionPixelSize(R.dimen.margin_extra_large) + tab_layout.setupWithViewPager(view_pager) + + jetpack_terms_and_conditions.text = Html.fromHtml( + String.format(resources.getString(R.string.jetpack_connection_terms_and_conditions), "", "") + ) + jetpack_terms_and_conditions.setOnClickListener { + WPWebViewActivity.openURL(requireContext(), WPUrlUtils.buildTermsOfServiceUrl(context)) + } + jetpack_faq.setOnClickListener { + WPWebViewActivity.openURL(requireContext(), StatsConnectJetpackActivity.FAQ_URL) + } + } + + private fun buildTitles(): List { + val result: ArrayList = ArrayList(TAB_COUNT) + result.add(TAB_POSITION_ALL, getString(R.string.notifications_tab_title_all)) + result.add(TAB_POSITION_UNREAD, getString(R.string.notifications_tab_title_unread_notifications)) + result.add(TAB_POSITION_COMMENT, getString(R.string.notifications_tab_title_comments)) + result.add(TAB_POSITION_FOLLOW, getString(R.string.notifications_tab_title_follows)) + result.add(TAB_POSITION_LIKE, getString(R.string.notifications_tab_title_likes)) + return result + } + + override fun onPause() { + super.onPause() + shouldRefreshNotifications = true + } + + override fun onResume() { + super.onResume() + EventBus.getDefault().post(NotificationsUnseenStatus(false)) + if (!accountStore.hasAccessToken()) { + showConnectJetpackView() + connect_jetpack.visibility = View.VISIBLE + tab_layout.visibility = View.GONE + view_pager.visibility = View.GONE + } else { + connect_jetpack.visibility = View.GONE + tab_layout.visibility = View.VISIBLE + view_pager.visibility = View.VISIBLE + if (shouldRefreshNotifications) { + fetchNotesFromRemote() + } + } + setSelectedTab(lastTabPosition) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putInt(KEY_LAST_TAB_POSITION, lastTabPosition) + super.onSaveInstanceState(outState) + } + + private fun clearToolbarScrollFlags() { + if (toolbar_main.layoutParams is LayoutParams) { + val params = toolbar_main.layoutParams as LayoutParams + params.scrollFlags = 0 + } + } + + private fun fetchNotesFromRemote() { + if (!isAdded || !NetworkUtils.isNetworkAvailable(activity)) { + return + } + NotificationsUpdateServiceStarter.startService(activity) + } + + private fun setSelectedTab(position: Int) { + lastTabPosition = position + tab_layout.getTabAt(lastTabPosition)?.select() + } + + private fun showConnectJetpackView() { + clearToolbarScrollFlags() + jetpack_setup.setOnClickListener { + val siteModel = (requireActivity() as? WPMainActivity)?.selectedSite + JetpackConnectionWebViewActivity.startJetpackConnectionFlow(activity, NOTIFICATIONS, siteModel, false) + } + } + + private class NotificationsFragmentAdapter internal constructor( + fragmentManager: FragmentManager, + private val titles: List + ) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + override fun getCount(): Int { + return TAB_COUNT + } + + override fun getItem(position: Int): Fragment { + return NotificationsListFragmentPage.newInstance(position) + } + + override fun getPageTitle(position: Int): CharSequence? { + if (titles.size > position && position >= 0) { + return titles[position] + } + return super.getPageTitle(position) + } + + override fun restoreState(state: Parcelable?, loader: ClassLoader?) { + try { + super.restoreState(state, loader) + } catch (exception: IllegalStateException) { + AppLog.e(NOTIFS, exception) + } + } + } + + override fun onPrepareOptionsMenu(menu: Menu) { + val notificationSettings = menu.findItem(R.id.notifications_settings) + notificationSettings.isVisible = accountStore.hasAccessToken() + super.onPrepareOptionsMenu(menu) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.notifications_list_menu, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.notifications_settings) { + ActivityLauncher.viewNotificationsSettings(activity) + return true + } + return super.onOptionsItemSelected(item) + } + + companion object { + const val NOTE_ID_EXTRA = "noteId" + const val NOTE_INSTANT_REPLY_EXTRA = "instantReply" + const val NOTE_PREFILLED_REPLY_EXTRA = "prefilledReplyText" + const val NOTE_MODERATE_ID_EXTRA = "moderateNoteId" + const val NOTE_MODERATE_STATUS_EXTRA = "moderateNoteStatus" + const val NOTE_CURRENT_LIST_FILTER_EXTRA = "currentFilter" + private const val TAB_COUNT = 5 + const val TAB_POSITION_ALL = 0 + const val TAB_POSITION_UNREAD = 1 + const val TAB_POSITION_COMMENT = 2 + const val TAB_POSITION_FOLLOW = 3 + const val TAB_POSITION_LIKE = 4 + private const val KEY_LAST_TAB_POSITION = "lastTabPosition" + fun newInstance(): NotificationsListFragment { + return NotificationsListFragment() + } + + private fun getOpenNoteIntent(activity: Activity, noteId: String): Intent { + val detailIntent = Intent(activity, NotificationsDetailActivity::class.java) + detailIntent.putExtra(NOTE_ID_EXTRA, noteId) + return detailIntent + } + + @JvmStatic fun openNoteForReply( + activity: Activity?, + noteId: String?, + shouldShowKeyboard: Boolean, + replyText: String?, + filter: FILTERS?, + isTappedFromPushNotification: Boolean + ) { + if (noteId == null || activity == null) { + return + } + if (activity.isFinishing) { + return + } + val detailIntent = getOpenNoteIntent(activity, noteId) + detailIntent.putExtra(NOTE_INSTANT_REPLY_EXTRA, shouldShowKeyboard) + if (!TextUtils.isEmpty(replyText)) { + detailIntent.putExtra(NOTE_PREFILLED_REPLY_EXTRA, replyText) + } + detailIntent.putExtra(NOTE_CURRENT_LIST_FILTER_EXTRA, filter) + detailIntent.putExtra(IS_TAPPED_ON_NOTIFICATION, isTappedFromPushNotification) + openNoteForReplyWithParams(detailIntent, activity) + } + + private fun openNoteForReplyWithParams(detailIntent: Intent, activity: Activity) { + activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.java deleted file mode 100644 index b348417cac2e..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.java +++ /dev/null @@ -1,614 +0,0 @@ -package org.wordpress.android.ui.notifications; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.android.volley.VolleyError; -import com.wordpress.rest.RestRequest; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.json.JSONObject; -import org.wordpress.android.R; -import org.wordpress.android.WordPress; -import org.wordpress.android.datasets.NotificationsTable; -import org.wordpress.android.fluxc.model.CommentStatus; -import org.wordpress.android.fluxc.model.SiteModel; -import org.wordpress.android.fluxc.store.AccountStore; -import org.wordpress.android.models.Note; -import org.wordpress.android.push.GCMMessageHandler; -import org.wordpress.android.ui.ActionableEmptyView; -import org.wordpress.android.ui.ActivityLauncher; -import org.wordpress.android.ui.PagePostCreationSourcesDetail; -import org.wordpress.android.ui.RequestCodes; -import org.wordpress.android.ui.main.WPMainActivity; -import org.wordpress.android.ui.main.WPMainActivity.OnScrollToTopListener; -import org.wordpress.android.ui.notifications.NotificationEvents.NoteLikeOrModerationStatusChanged; -import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsChanged; -import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsRefreshCompleted; -import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsRefreshError; -import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsUnseenStatus; -import org.wordpress.android.ui.notifications.adapters.NotesAdapter; -import org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter; -import org.wordpress.android.ui.notifications.utils.NotificationsActions; -import org.wordpress.android.util.AniUtils; -import org.wordpress.android.util.CrashLoggingUtils; -import org.wordpress.android.util.DisplayUtils; -import org.wordpress.android.util.NetworkUtils; -import org.wordpress.android.util.helpers.SwipeToRefreshHelper; -import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout; -import org.wordpress.android.widgets.AppRatingDialog; - -import javax.inject.Inject; - -import static android.app.Activity.RESULT_OK; -import static org.wordpress.android.analytics.AnalyticsTracker.Stat.APP_REVIEWS_EVENT_INCREMENTED_BY_CHECKING_NOTIFICATION; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.NOTE_CURRENT_LIST_FILTER_EXTRA; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.NOTE_ID_EXTRA; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.NOTE_INSTANT_REPLY_EXTRA; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.NOTE_MODERATE_ID_EXTRA; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.NOTE_MODERATE_STATUS_EXTRA; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.NOTE_PREFILLED_REPLY_EXTRA; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.TAB_POSITION_ALL; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.TAB_POSITION_COMMENT; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.TAB_POSITION_FOLLOW; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.TAB_POSITION_LIKE; -import static org.wordpress.android.ui.notifications.NotificationsListFragment.TAB_POSITION_UNREAD; -import static org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_ALL; -import static org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_COMMENT; -import static org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_FOLLOW; -import static org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_LIKE; -import static org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_UNREAD; -import static org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter.IS_TAPPED_ON_NOTIFICATION; -import static org.wordpress.android.util.WPSwipeToRefreshHelper.buildSwipeToRefreshHelper; - -public class NotificationsListFragmentPage extends Fragment implements - OnScrollToTopListener, - NotesAdapter.DataLoadedListener { - private static final String KEY_TAB_POSITION = "tabPosition"; - - private ActionableEmptyView mActionableEmptyView; - private LinearLayoutManager mLinearLayoutManager; - private NotesAdapter mNotesAdapter; - private RecyclerView mRecyclerView; - private SwipeToRefreshHelper mSwipeToRefreshHelper; - private View mNewNotificationsBar; - private boolean mIsAnimatingOutNewNotificationsBar; - private boolean mShouldRefreshNotifications; - private int mTabPosition; - - @Inject AccountStore mAccountStore; - @Inject GCMMessageHandler mGCMMessageHandler; - - public static Fragment newInstance(int position) { - NotificationsListFragmentPage fragment = new NotificationsListFragmentPage(); - Bundle bundle = new Bundle(); - bundle.putInt(KEY_TAB_POSITION, position); - fragment.setArguments(bundle); - return fragment; - } - - public interface OnNoteClickListener { - void onClickNote(String noteId); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mRecyclerView.setAdapter(getNotesAdapter()); - - if (savedInstanceState != null) { - mTabPosition = savedInstanceState.getInt(KEY_TAB_POSITION, TAB_POSITION_ALL); - } - - switch (mTabPosition) { - case TAB_POSITION_ALL: - mNotesAdapter.setFilter(FILTER_ALL); - break; - case TAB_POSITION_COMMENT: - mNotesAdapter.setFilter(FILTER_COMMENT); - break; - case TAB_POSITION_FOLLOW: - mNotesAdapter.setFilter(FILTER_FOLLOW); - break; - case TAB_POSITION_LIKE: - mNotesAdapter.setFilter(FILTER_LIKE); - break; - case TAB_POSITION_UNREAD: - mNotesAdapter.setFilter(FILTER_UNREAD); - break; - default: - mNotesAdapter.setFilter(FILTER_ALL); - break; - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == RequestCodes.NOTE_DETAIL) { - mShouldRefreshNotifications = false; - - if (resultCode == RESULT_OK) { - String noteId = data.getStringExtra(NOTE_MODERATE_ID_EXTRA); - String newStatus = data.getStringExtra(NOTE_MODERATE_STATUS_EXTRA); - - if (!TextUtils.isEmpty(noteId) && !TextUtils.isEmpty(newStatus)) { - updateNote(noteId, CommentStatus.fromString(newStatus)); - } - } - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ((WordPress) requireActivity().getApplication()).component().inject(this); - mShouldRefreshNotifications = true; - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.notifications_list_fragment_page, container, false); - - if (getArguments() != null) { - mTabPosition = getArguments().getInt(KEY_TAB_POSITION, TAB_POSITION_ALL); - } - - mActionableEmptyView = view.findViewById(R.id.actionable_empty_view); - - mLinearLayoutManager = new LinearLayoutManager(getActivity()); - mRecyclerView = view.findViewById(R.id.notifications_list); - mRecyclerView.setLayoutManager(mLinearLayoutManager); - - mSwipeToRefreshHelper = buildSwipeToRefreshHelper( - (CustomSwipeRefreshLayout) view.findViewById(R.id.notifications_refresh), - new SwipeToRefreshHelper.RefreshListener() { - @Override - public void onRefreshStarted() { - hideNewNotificationsBar(); - fetchNotesFromRemote(); - } - }); - - mNewNotificationsBar = view.findViewById(R.id.layout_new_notificatons); - mNewNotificationsBar.setVisibility(View.GONE); - mNewNotificationsBar.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - onScrollToTop(); - } - }); - - return view; - } - - @Override - public void onDataLoaded(int itemsCount) { - if (!isAdded()) { - CrashLoggingUtils.log("NotificationsListFragmentPage.onDataLoaded occurred when fragment is not attached."); - } - - if (itemsCount > 0) { - hideEmptyView(); - } else { - showEmptyViewForCurrentFilter(); - } - } - - @Override - public void onPause() { - super.onPause(); - mShouldRefreshNotifications = true; - } - - @Override - public void onResume() { - super.onResume(); - hideNewNotificationsBar(); - EventBus.getDefault().post(new NotificationsUnseenStatus(false)); - - if (mAccountStore.hasAccessToken()) { - getNotesAdapter().reloadNotesFromDBAsync(); - - if (mShouldRefreshNotifications) { - fetchNotesFromRemote(); - } - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putInt(KEY_TAB_POSITION, mTabPosition); - super.onSaveInstanceState(outState); - } - - @Override - public void onScrollToTop() { - if (!isAdded()) { - return; - } - - clearPendingNotificationsItemsOnUI(); - - if (mLinearLayoutManager.findFirstCompletelyVisibleItemPosition() > 0) { - mLinearLayoutManager.smoothScrollToPosition(mRecyclerView, null, 0); - } - } - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - } - - @Override - public void onStop() { - EventBus.getDefault().unregister(this); - super.onStop(); - } - - private final OnNoteClickListener mOnNoteClickListener = new OnNoteClickListener() { - @Override - public void onClickNote(String noteId) { - if (!isAdded()) { - return; - } - - if (TextUtils.isEmpty(noteId)) { - return; - } - - AppRatingDialog.INSTANCE.incrementInteractions(APP_REVIEWS_EVENT_INCREMENTED_BY_CHECKING_NOTIFICATION); - - // Open the latest version of this note in case it has changed, which can happen if the note was tapped - // from the list after it was updated by another fragment (such as NotificationsDetailListFragment). - openNoteForReply(getActivity(), noteId, false, null, mNotesAdapter.getCurrentFilter(), false); - } - }; - - private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); - mRecyclerView.removeOnScrollListener(this); - clearPendingNotificationsItemsOnUI(); - } - }; - - private void clearPendingNotificationsItemsOnUI() { - hideNewNotificationsBar(); - EventBus.getDefault().post(new NotificationsUnseenStatus(false)); - NotificationsActions.updateNotesSeenTimestamp(); - - new Thread(new Runnable() { - public void run() { - mGCMMessageHandler.removeAllNotifications(getActivity()); - } - }).start(); - } - - private void fetchNotesFromRemote() { - if (!isAdded() || mNotesAdapter == null) { - return; - } - - if (!NetworkUtils.isNetworkAvailable(getActivity())) { - mSwipeToRefreshHelper.setRefreshing(false); - return; - } - - NotificationsUpdateServiceStarter.startService(getActivity()); - } - - private NotesAdapter getNotesAdapter() { - if (mNotesAdapter == null) { - mNotesAdapter = new NotesAdapter(requireActivity(), this, null); - mNotesAdapter.setOnNoteClickListener(mOnNoteClickListener); - } - - return mNotesAdapter; - } - - private static Intent getOpenNoteIntent(Activity activity, String noteId) { - Intent detailIntent = new Intent(activity, NotificationsDetailActivity.class); - detailIntent.putExtra(NOTE_ID_EXTRA, noteId); - return detailIntent; - } - - public SiteModel getSelectedSite() { - if (getActivity() instanceof WPMainActivity) { - return ((WPMainActivity) getActivity()).getSelectedSite(); - } else { - return null; - } - } - - private void hideEmptyView() { - if (isAdded() && mActionableEmptyView != null) { - mActionableEmptyView.setVisibility(View.GONE); - mRecyclerView.setVisibility(View.VISIBLE); - } - } - - private void hideNewNotificationsBar() { - if (!isAdded() || !isNewNotificationsBarShowing() || mIsAnimatingOutNewNotificationsBar) { - return; - } - - mIsAnimatingOutNewNotificationsBar = true; - - Animation.AnimationListener listener = new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - if (isAdded()) { - mNewNotificationsBar.setVisibility(View.GONE); - mIsAnimatingOutNewNotificationsBar = false; - } - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - }; - AniUtils.startAnimation(mNewNotificationsBar, R.anim.notifications_bottom_bar_out, listener); - } - - private boolean isNewNotificationsBarShowing() { - return (mNewNotificationsBar != null && mNewNotificationsBar.getVisibility() == View.VISIBLE); - } - - public static void openNoteForReply(Activity activity, String noteId, boolean shouldShowKeyboard, String replyText, - NotesAdapter.FILTERS filter, boolean isTappedFromPushNotification) { - if (noteId == null || activity == null || activity.isFinishing()) { - return; - } - - Intent detailIntent = getOpenNoteIntent(activity, noteId); - detailIntent.putExtra(NOTE_INSTANT_REPLY_EXTRA, shouldShowKeyboard); - - if (!TextUtils.isEmpty(replyText)) { - detailIntent.putExtra(NOTE_PREFILLED_REPLY_EXTRA, replyText); - } - - detailIntent.putExtra(NOTE_CURRENT_LIST_FILTER_EXTRA, filter); - detailIntent.putExtra(IS_TAPPED_ON_NOTIFICATION, isTappedFromPushNotification); - openNoteForReplyWithParams(detailIntent, activity); - } - - private static void openNoteForReplyWithParams(Intent detailIntent, Activity activity) { - activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL); - } - - private void performActionForActiveFilter() { - if (!isAdded()) { - return; - } - - if (!mAccountStore.hasAccessToken()) { - ActivityLauncher.showSignInForResult(getActivity()); - return; - } - - if (mTabPosition == TAB_POSITION_UNREAD) { - ActivityLauncher.addNewPostForResult( - getActivity(), - getSelectedSite(), - false, - PagePostCreationSourcesDetail.POST_FROM_NOTIFS_EMPTY_VIEW - ); - } else if (getActivity() instanceof WPMainActivity) { - ((WPMainActivity) getActivity()).setReaderPageActive(); - } - } - - private void showEmptyView(@StringRes int titleResId) { - showEmptyView(titleResId, 0, 0); - } - - private void showEmptyView(@StringRes int titleResId, @StringRes int descriptionResId, @StringRes int buttonResId) { - if (isAdded() && mActionableEmptyView != null) { - mActionableEmptyView.setVisibility(View.VISIBLE); - mRecyclerView.setVisibility(View.GONE); - mActionableEmptyView.title.setText(titleResId); - - if (descriptionResId != 0) { - mActionableEmptyView.subtitle.setText(descriptionResId); - mActionableEmptyView.subtitle.setVisibility(View.VISIBLE); - } else { - mActionableEmptyView.subtitle.setVisibility(View.GONE); - } - - if (buttonResId != 0) { - mActionableEmptyView.button.setText(buttonResId); - mActionableEmptyView.button.setVisibility(View.VISIBLE); - } else { - mActionableEmptyView.button.setVisibility(View.GONE); - } - - mActionableEmptyView.button.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - performActionForActiveFilter(); - } - }); - } - } - - // Show different empty view message and action button based on selected tab. - private void showEmptyViewForCurrentFilter() { - if (!mAccountStore.hasAccessToken()) { - return; - } - - switch (mTabPosition) { - case TAB_POSITION_ALL: - showEmptyView( - R.string.notifications_empty_all, - R.string.notifications_empty_action_all, - R.string.notifications_empty_view_reader); - break; - case TAB_POSITION_COMMENT: - showEmptyView( - R.string.notifications_empty_comments, - R.string.notifications_empty_action_comments, - R.string.notifications_empty_view_reader); - break; - case TAB_POSITION_FOLLOW: - showEmptyView( - R.string.notifications_empty_followers, - R.string.notifications_empty_action_followers_likes, - R.string.notifications_empty_view_reader); - break; - case TAB_POSITION_LIKE: - showEmptyView( - R.string.notifications_empty_likes, - R.string.notifications_empty_action_followers_likes, - R.string.notifications_empty_view_reader); - break; - case TAB_POSITION_UNREAD: - if (getSelectedSite() == null) { - showEmptyView(R.string.notifications_empty_unread); - } else { - showEmptyView( - R.string.notifications_empty_unread, - R.string.notifications_empty_action_unread, - R.string.posts_empty_list_button); - } - - break; - default: - showEmptyView(R.string.notifications_empty_list); - } - - mActionableEmptyView.image.setVisibility(DisplayUtils.isLandscape(getContext()) ? View.GONE : View.VISIBLE); - } - - private void showNewNotificationsBar() { - if (!isAdded() || isNewNotificationsBarShowing()) { - return; - } - - AniUtils.startAnimation(mNewNotificationsBar, R.anim.notifications_bottom_bar_in); - mNewNotificationsBar.setVisibility(View.VISIBLE); - } - - private void showNewUnseenNotificationsUI() { - if (!isAdded() || mRecyclerView == null || mRecyclerView.getLayoutManager() == null) { - return; - } - - mRecyclerView.clearOnScrollListeners(); - mRecyclerView.postDelayed(new Runnable() { - @Override - public void run() { - if (isAdded()) { - mRecyclerView.addOnScrollListener(mOnScrollListener); - } - } - }, 1000L); - - View first = mRecyclerView.getLayoutManager().getChildAt(0); - // Show new notifications bar if first item is not visible on the screen. - if (first != null && mRecyclerView.getLayoutManager().getPosition(first) > 0) { - showNewNotificationsBar(); - } - } - - private void updateNote(String noteId, CommentStatus status) { - Note note = NotificationsTable.getNoteById(noteId); - - if (note != null) { - note.setLocalStatus(status.toString()); - NotificationsTable.saveNote(note); - EventBus.getDefault().post(new NotificationsChanged()); - } - } - - @SuppressWarnings("unused") - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(final NoteLikeOrModerationStatusChanged event) { - NotificationsActions.downloadNoteAndUpdateDB( - event.noteId, - new RestRequest.Listener() { - @Override - public void onResponse(JSONObject response) { - EventBus.getDefault().removeStickyEvent(NoteLikeOrModerationStatusChanged.class); - Note note = NotificationsTable.getNoteById(event.noteId); - - if (note != null) { - mNotesAdapter.replaceNote(note); - } - } - }, new RestRequest.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - EventBus.getDefault().removeStickyEvent(NoteLikeOrModerationStatusChanged.class); - } - } - ); - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(NotificationsChanged event) { - if (!isAdded()) { - return; - } - - getNotesAdapter().reloadNotesFromDBAsync(); - - if (event.hasUnseenNotes) { - showNewUnseenNotificationsUI(); - } - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(final NotificationsRefreshCompleted event) { - if (!isAdded()) { - return; - } - - mSwipeToRefreshHelper.setRefreshing(false); - mNotesAdapter.addAll(event.notes, true); - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(NotificationsRefreshError error) { - if (isAdded()) { - mSwipeToRefreshHelper.setRefreshing(false); - } - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(NotificationsUnseenStatus event) { - if (!isAdded()) { - return; - } - - if (event.hasUnseenNotes) { - showNewUnseenNotificationsUI(); - } else { - hideNewNotificationsBar(); - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt new file mode 100644 index 000000000000..986987b9be79 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -0,0 +1,487 @@ +package org.wordpress.android.ui.notifications + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.Animation.AnimationListener +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.OnScrollListener +import kotlinx.android.synthetic.main.notifications_list_fragment_page.* +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode.MAIN +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.analytics.AnalyticsTracker.Stat.APP_REVIEWS_EVENT_INCREMENTED_BY_CHECKING_NOTIFICATION +import org.wordpress.android.datasets.NotificationsTable +import org.wordpress.android.fluxc.model.CommentStatus +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.push.GCMMessageHandler +import org.wordpress.android.ui.ActivityLauncher +import org.wordpress.android.ui.PagePostCreationSourcesDetail.POST_FROM_NOTIFS_EMPTY_VIEW +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.ui.main.WPMainActivity +import org.wordpress.android.ui.main.WPMainActivity.OnScrollToTopListener +import org.wordpress.android.ui.notifications.NotificationEvents.NoteLikeOrModerationStatusChanged +import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsChanged +import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsRefreshCompleted +import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsRefreshError +import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsUnseenStatus +import org.wordpress.android.ui.notifications.adapters.NotesAdapter +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.DataLoadedListener +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_ALL +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_COMMENT +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_FOLLOW +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_LIKE +import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILTER_UNREAD +import org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter +import org.wordpress.android.ui.notifications.utils.NotificationsActions +import org.wordpress.android.util.AniUtils +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T +import org.wordpress.android.util.DisplayUtils +import org.wordpress.android.util.NetworkUtils +import org.wordpress.android.util.WPSwipeToRefreshHelper +import org.wordpress.android.util.helpers.SwipeToRefreshHelper +import org.wordpress.android.widgets.AppRatingDialog.incrementInteractions +import javax.inject.Inject + +class NotificationsListFragmentPage : Fragment(), OnScrollToTopListener, DataLoadedListener { + private var notesAdapter: NotesAdapter? = null + private var swipeToRefreshHelper: SwipeToRefreshHelper? = null + private var isAnimatingOutNewNotificationsBar = false + private var shouldRefreshNotifications = false + private var tabPosition = 0 + + @Inject lateinit var accountStore: AccountStore + @Inject lateinit var gcmMessageHandler: GCMMessageHandler + + interface OnNoteClickListener { + fun onClickNote(noteId: String?) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + val adapter = createOrGetNotesAdapter() + notifications_list.adapter = adapter + if (savedInstanceState != null) { + tabPosition = savedInstanceState.getInt(KEY_TAB_POSITION, NotificationsListFragment.TAB_POSITION_ALL) + } + when (tabPosition) { + NotificationsListFragment.TAB_POSITION_ALL -> adapter.setFilter(FILTER_ALL) + NotificationsListFragment.TAB_POSITION_COMMENT -> adapter.setFilter(FILTER_COMMENT) + NotificationsListFragment.TAB_POSITION_FOLLOW -> adapter.setFilter(FILTER_FOLLOW) + NotificationsListFragment.TAB_POSITION_LIKE -> adapter.setFilter(FILTER_LIKE) + NotificationsListFragment.TAB_POSITION_UNREAD -> adapter.setFilter(FILTER_UNREAD) + else -> adapter.setFilter(FILTER_ALL) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == RequestCodes.NOTE_DETAIL) { + shouldRefreshNotifications = false + if (resultCode == Activity.RESULT_OK) { + val noteId = data?.getStringExtra(NotificationsListFragment.NOTE_MODERATE_ID_EXTRA) + val newStatus = data?.getStringExtra(NotificationsListFragment.NOTE_MODERATE_STATUS_EXTRA) + if (!noteId.isNullOrBlank() && !newStatus.isNullOrBlank()) { + updateNote(noteId, CommentStatus.fromString(newStatus)) + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + shouldRefreshNotifications = true + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.notifications_list_fragment_page, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + arguments?.let { + tabPosition = it.getInt(KEY_TAB_POSITION, NotificationsListFragment.TAB_POSITION_ALL) + } + notifications_list.layoutManager = LinearLayoutManager(activity) + swipeToRefreshHelper = WPSwipeToRefreshHelper.buildSwipeToRefreshHelper(notifications_refresh) { + hideNewNotificationsBar() + fetchNotesFromRemote() + } + layout_new_notificatons.visibility = View.GONE + layout_new_notificatons.setOnClickListener { onScrollToTop() } + } + + override fun onDestroyView() { + swipeToRefreshHelper = null + notifications_list.adapter = null + notesAdapter = null + super.onDestroyView() + } + + override fun onDataLoaded(itemsCount: Int) { + if (!isAdded) { + AppLog.d(T.NOTIFS, + "NotificationsListFragmentPage.onDataLoaded occurred when fragment is not attached.") + } + if (itemsCount > 0) { + hideEmptyView() + } else { + showEmptyViewForCurrentFilter() + } + } + + override fun onPause() { + super.onPause() + shouldRefreshNotifications = true + } + + override fun onResume() { + super.onResume() + hideNewNotificationsBar() + EventBus.getDefault().post(NotificationsUnseenStatus(false)) + if (accountStore.hasAccessToken()) { + notesAdapter!!.reloadNotesFromDBAsync() + if (shouldRefreshNotifications) { + fetchNotesFromRemote() + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putInt(KEY_TAB_POSITION, tabPosition) + super.onSaveInstanceState(outState) + } + + override fun onScrollToTop() { + if (!isAdded) { + return + } + clearPendingNotificationsItemsOnUI() + val layoutManager = notifications_list.layoutManager as LinearLayoutManager + if (layoutManager.findFirstCompletelyVisibleItemPosition() > 0) { + layoutManager.smoothScrollToPosition(notifications_list, null, 0) + } + } + + override fun onStart() { + super.onStart() + EventBus.getDefault().register(this) + } + + override fun onStop() { + EventBus.getDefault().unregister(this) + super.onStop() + } + + private val mOnNoteClickListener: OnNoteClickListener = object : OnNoteClickListener { + override fun onClickNote(noteId: String?) { + if (!isAdded) { + return + } + if (TextUtils.isEmpty(noteId)) { + return + } + incrementInteractions(APP_REVIEWS_EVENT_INCREMENTED_BY_CHECKING_NOTIFICATION) + + // Open the latest version of this note in case it has changed, which can happen if the note was tapped + // from the list after it was updated by another fragment (such as NotificationsDetailListFragment). + openNoteForReply(activity, noteId, false, null, notesAdapter!!.currentFilter, false) + } + } + private val mOnScrollListener: OnScrollListener = object : OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + notifications_list.removeOnScrollListener(this) + clearPendingNotificationsItemsOnUI() + } + } + + private fun clearPendingNotificationsItemsOnUI() { + hideNewNotificationsBar() + EventBus.getDefault().post(NotificationsUnseenStatus(false)) + NotificationsActions.updateNotesSeenTimestamp() + Thread(Runnable { gcmMessageHandler.removeAllNotifications(activity) }).start() + } + + private fun fetchNotesFromRemote() { + if (!isAdded || notesAdapter == null) { + return + } + if (!NetworkUtils.isNetworkAvailable(activity)) { + swipeToRefreshHelper?.isRefreshing = false + return + } + NotificationsUpdateServiceStarter.startService(activity) + } + + val selectedSite: SiteModel? + get() = (activity as? WPMainActivity)?.selectedSite + + private fun hideEmptyView() { + if (isAdded) { + actionable_empty_view.visibility = View.GONE + notifications_list.visibility = View.VISIBLE + } + } + + private fun hideNewNotificationsBar() { + if (!isAdded || !isNewNotificationsBarShowing || isAnimatingOutNewNotificationsBar) { + return + } + isAnimatingOutNewNotificationsBar = true + val listener: AnimationListener = object : AnimationListener { + override fun onAnimationStart(animation: Animation) {} + override fun onAnimationEnd(animation: Animation) { + if (isAdded) { + layout_new_notificatons.visibility = View.GONE + isAnimatingOutNewNotificationsBar = false + } + } + + override fun onAnimationRepeat(animation: Animation) {} + } + AniUtils.startAnimation(layout_new_notificatons, R.anim.notifications_bottom_bar_out, listener) + } + + private val isNewNotificationsBarShowing: Boolean + get() = layout_new_notificatons != null && layout_new_notificatons.visibility == View.VISIBLE + + private fun performActionForActiveFilter() { + if (!isAdded) { + return + } + if (!accountStore.hasAccessToken()) { + ActivityLauncher.showSignInForResult(activity) + return + } + if (tabPosition == NotificationsListFragment.TAB_POSITION_UNREAD) { + ActivityLauncher.addNewPostForResult(activity, selectedSite, false, POST_FROM_NOTIFS_EMPTY_VIEW) + } else if (activity is WPMainActivity) { + (requireActivity() as WPMainActivity).setReaderPageActive() + } + } + + private fun showEmptyView( + @StringRes titleResId: Int, + @StringRes descriptionResId: Int = 0, + @StringRes buttonResId: Int = 0 + ) { + if (isAdded) { + actionable_empty_view.visibility = View.VISIBLE + notifications_list.visibility = View.GONE + actionable_empty_view.title.setText(titleResId) + if (descriptionResId != 0) { + actionable_empty_view.subtitle.setText(descriptionResId) + actionable_empty_view.subtitle.visibility = View.VISIBLE + } else { + actionable_empty_view.subtitle.visibility = View.GONE + } + if (buttonResId != 0) { + actionable_empty_view.button.setText(buttonResId) + actionable_empty_view.button.visibility = View.VISIBLE + } else { + actionable_empty_view.button.visibility = View.GONE + } + actionable_empty_view.button.setOnClickListener { performActionForActiveFilter() } + } + } + + // Show different empty view message and action button based on selected tab. + private fun showEmptyViewForCurrentFilter() { + if (!accountStore.hasAccessToken()) { + return + } + when (tabPosition) { + NotificationsListFragment.TAB_POSITION_ALL -> showEmptyView( + R.string.notifications_empty_all, + R.string.notifications_empty_action_all, + R.string.notifications_empty_view_reader + ) + NotificationsListFragment.TAB_POSITION_COMMENT -> showEmptyView( + R.string.notifications_empty_comments, + R.string.notifications_empty_action_comments, + R.string.notifications_empty_view_reader + ) + NotificationsListFragment.TAB_POSITION_FOLLOW -> showEmptyView( + R.string.notifications_empty_followers, + R.string.notifications_empty_action_followers_likes, + R.string.notifications_empty_view_reader + ) + NotificationsListFragment.TAB_POSITION_LIKE -> showEmptyView( + R.string.notifications_empty_likes, + R.string.notifications_empty_action_followers_likes, + R.string.notifications_empty_view_reader + ) + NotificationsListFragment.TAB_POSITION_UNREAD -> if (selectedSite == null) { + showEmptyView(R.string.notifications_empty_unread) + } else { + showEmptyView( + R.string.notifications_empty_unread, + R.string.notifications_empty_action_unread, + R.string.posts_empty_list_button + ) + } + else -> showEmptyView(R.string.notifications_empty_list) + } + actionable_empty_view.image.visibility = if (DisplayUtils.isLandscape(context)) View.GONE else View.VISIBLE + } + + private fun showNewNotificationsBar() { + if (!isAdded || isNewNotificationsBarShowing) { + return + } + AniUtils.startAnimation(layout_new_notificatons, R.anim.notifications_bottom_bar_in) + layout_new_notificatons.visibility = View.VISIBLE + } + + private fun showNewUnseenNotificationsUI() { + if (!isAdded || notifications_list.layoutManager == null) { + return + } + notifications_list.clearOnScrollListeners() + notifications_list.postDelayed({ + if (isAdded) { + notifications_list.addOnScrollListener(mOnScrollListener) + } + }, 1000L) + val first = notifications_list.layoutManager!!.getChildAt(0) + // Show new notifications bar if first item is not visible on the screen. + if (first != null && notifications_list.layoutManager!!.getPosition(first) > 0) { + showNewNotificationsBar() + } + } + + private fun updateNote(noteId: String, status: CommentStatus) { + val note = NotificationsTable.getNoteById(noteId) + if (note != null) { + note.localStatus = status.toString() + NotificationsTable.saveNote(note) + EventBus.getDefault().post(NotificationsChanged()) + } + } + + private fun createOrGetNotesAdapter(): NotesAdapter { + return notesAdapter ?: NotesAdapter(requireActivity(), this, null).apply { + notesAdapter = this + this.setOnNoteClickListener(mOnNoteClickListener) + } + } + + @Subscribe(sticky = true, threadMode = MAIN) + fun onEventMainThread(event: NoteLikeOrModerationStatusChanged) { + NotificationsActions.downloadNoteAndUpdateDB( + event.noteId, + { + EventBus.getDefault() + .removeStickyEvent( + NoteLikeOrModerationStatusChanged::class.java + ) + val note = NotificationsTable.getNoteById(event.noteId) + if (note != null) { + notesAdapter!!.replaceNote(note) + } + } + ) { + EventBus.getDefault().removeStickyEvent( + NoteLikeOrModerationStatusChanged::class.java + ) + } + } + + @Subscribe(threadMode = MAIN) + fun onEventMainThread(event: NotificationsChanged) { + if (!isAdded) { + return + } + notesAdapter!!.reloadNotesFromDBAsync() + if (event.hasUnseenNotes) { + showNewUnseenNotificationsUI() + } + } + + @Subscribe(threadMode = MAIN) + fun onEventMainThread(event: NotificationsRefreshCompleted) { + if (!isAdded) { + return + } + swipeToRefreshHelper?.isRefreshing = false + notesAdapter!!.addAll(event.notes, true) + } + + @Subscribe(threadMode = MAIN) + fun onEventMainThread(error: NotificationsRefreshError?) { + if (isAdded) { + swipeToRefreshHelper?.isRefreshing = false + } + } + + @Subscribe(threadMode = MAIN) + fun onEventMainThread(event: NotificationsUnseenStatus) { + if (!isAdded) { + return + } + if (event.hasUnseenNotes) { + showNewUnseenNotificationsUI() + } else { + hideNewNotificationsBar() + } + } + + companion object { + private const val KEY_TAB_POSITION = "tabPosition" + fun newInstance(position: Int): Fragment { + val fragment = NotificationsListFragmentPage() + val bundle = Bundle() + bundle.putInt(KEY_TAB_POSITION, position) + fragment.arguments = bundle + return fragment + } + + private fun getOpenNoteIntent(activity: Activity, noteId: String): Intent { + val detailIntent = Intent(activity, NotificationsDetailActivity::class.java) + detailIntent.putExtra(NotificationsListFragment.NOTE_ID_EXTRA, noteId) + return detailIntent + } + + fun openNoteForReply( + activity: Activity?, + noteId: String?, + shouldShowKeyboard: Boolean, + replyText: String?, + filter: FILTERS?, + isTappedFromPushNotification: Boolean + ) { + if (noteId == null || activity == null || activity.isFinishing) { + return + } + val detailIntent = getOpenNoteIntent(activity, noteId) + detailIntent.putExtra(NotificationsListFragment.NOTE_INSTANT_REPLY_EXTRA, shouldShowKeyboard) + if (!TextUtils.isEmpty(replyText)) { + detailIntent.putExtra(NotificationsListFragment.NOTE_PREFILLED_REPLY_EXTRA, replyText) + } + detailIntent.putExtra(NotificationsListFragment.NOTE_CURRENT_LIST_FILTER_EXTRA, filter) + detailIntent.putExtra( + NotificationsUpdateServiceStarter.IS_TAPPED_ON_NOTIFICATION, + isTappedFromPushNotification + ) + openNoteForReplyWithParams(detailIntent, activity) + } + + private fun openNoteForReplyWithParams(detailIntent: Intent, activity: Activity) { + activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerAdapter.java index 7e0b5c269148..a562dc67e4c3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerAdapter.java @@ -286,6 +286,16 @@ private ThumbnailViewHolder getViewHolderAtPosition(int position) { return (ThumbnailViewHolder) mRecycler.findViewHolderForAdapterPosition(position); } + boolean isVideoFileSelected() { + for (Integer position : mSelectedPositions) { + PhotoPickerItem item = getItemAtPosition(position); + if (item != null && item.mIsVideo) { + return true; + } + } + return false; + } + @NonNull ArrayList getSelectedURIs() { ArrayList uriList = new ArrayList<>(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java index 7b0289814353..f9c379762c2d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java @@ -13,6 +13,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.PopupMenu; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -90,7 +91,7 @@ public interface PhotoPickerListener { private EmptyViewRecyclerView mRecycler; private PhotoPickerAdapter mAdapter; private View mMediaSourceBottomBar; - private View mInsertPreviewBottomBar; + private View mInsertEditBottomBar; private ActionableEmptyView mSoftAskView; private ActionMode mActionMode; private GridLayoutManager mGridManager; @@ -162,7 +163,7 @@ public void onScrollStateChanged(RecyclerView recyclerView, int newState) { }); mMediaSourceBottomBar = view.findViewById(R.id.container_media_source_bar); - mInsertPreviewBottomBar = view.findViewById(R.id.container_insert_preview_bar); + mInsertEditBottomBar = view.findViewById(R.id.container_insert_edit_bar); if (!canShowMediaSourceBottomBar()) { mMediaSourceBottomBar.setVisibility(View.GONE); @@ -213,14 +214,14 @@ public void onClick(View v) { } } - if (canShowInsertPreviewBottomBar()) { - mInsertPreviewBottomBar.findViewById(R.id.text_preview).setOnClickListener(v -> { + if (canShowInsertEditBottomBar()) { + mInsertEditBottomBar.findViewById(R.id.text_edit).setOnClickListener(v -> { ArrayList inputData = WPMediaUtils.createListOfEditImageInputData(requireContext(), getAdapter().getSelectedURIs()); ActivityLauncher.openImageEditor(getActivity(), inputData); }); - mInsertPreviewBottomBar.findViewById(R.id.text_insert).setOnClickListener(v -> performInsertAction()); + mInsertEditBottomBar.findViewById(R.id.text_insert).setOnClickListener(v -> performInsertAction()); } mSoftAskView = view.findViewById(R.id.soft_ask_view); @@ -238,8 +239,8 @@ private boolean canShowMediaSourceBottomBar() { return true; } - private boolean canShowInsertPreviewBottomBar() { - return mBrowserType.isGutenbergPicker() && !mBrowserType.isVideoPicker(); + private boolean canShowInsertEditBottomBar() { + return mBrowserType.isGutenbergPicker(); } @Override @@ -404,6 +405,13 @@ public void onSelectedCountChanged(int count) { if (activity == null) { return; } + + if (canShowInsertEditBottomBar()) { + TextView editView = mInsertEditBottomBar.findViewById(R.id.text_edit); + boolean isVideoFileSelected = getAdapter().isVideoFileSelected(); + editView.setVisibility(isVideoFileSelected ? View.GONE : View.VISIBLE); + } + if (mActionMode == null) { ((AppCompatActivity) activity).startSupportActionMode(new ActionModeCallback()); } @@ -511,8 +519,8 @@ private final class ActionModeCallback implements ActionMode.Callback { @Override public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { mActionMode = actionMode; - if (canShowInsertPreviewBottomBar()) { - showBottomBar(mInsertPreviewBottomBar); + if (canShowInsertEditBottomBar()) { + showBottomBar(mInsertEditBottomBar); } else { MenuInflater inflater = actionMode.getMenuInflater(); inflater.inflate(R.menu.photo_picker_action_mode, menu); @@ -542,7 +550,7 @@ public void onDestroyActionMode(ActionMode mode) { if (canShowMediaSourceBottomBar()) { showBottomBar(mMediaSourceBottomBar); } - hideBottomBar(mInsertPreviewBottomBar); + hideBottomBar(mInsertEditBottomBar); getAdapter().clearSelection(); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansViewModel.kt index 9a18d62bcd16..42fdbcba8f19 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansViewModel.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.action.PlanOffersAction.FETCH_PLAN_OFFERS import org.wordpress.android.fluxc.generated.PlanOffersActionBuilder @@ -17,6 +18,7 @@ import org.wordpress.android.modules.UI_SCOPE import org.wordpress.android.ui.plans.PlansViewModel.PlansListStatus.DONE import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject import javax.inject.Named @@ -25,7 +27,8 @@ class PlansViewModel @Inject constructor( private val dispatcher: Dispatcher, @Suppress("unused") private var plansStore: PlanOffersStore, - @param:Named(UI_SCOPE) private val uiScope: CoroutineScope + @param:Named(UI_SCOPE) private val uiScope: CoroutineScope, + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper ) : ViewModel() { enum class PlansListStatus { DONE, @@ -77,6 +80,7 @@ class PlansViewModel @Inject constructor( } fun onItemClicked(item: PlanOffersModel) { + analyticsTrackerWrapper.track(Stat.OPENED_PLANS_COMPARISON) _showDialog.value = item } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginDetailActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginDetailActivity.java index 4bb164dee36a..669e470d009d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginDetailActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginDetailActivity.java @@ -813,9 +813,9 @@ private void showSuccessfulInstallSnackbar() { .show(); } - private void showSuccessfulPluginRemovedSnackbar() { + private void showSuccessfulPluginRemovedSnackbar(String pluginDisplayName) { WPSnackbar.make(mContainer, - getString(R.string.plugin_removed_successfully, mPlugin.getDisplayName()), + getString(R.string.plugin_removed_successfully, pluginDisplayName), Snackbar.LENGTH_LONG) .show(); } @@ -1130,13 +1130,14 @@ public void onSitePluginDeleted(OnSitePluginDeleted event) { return; } + String pluginDisplayName = mPlugin.getDisplayName(); mIsRemovingPlugin = false; cancelRemovePluginProgressDialog(); if (event.isError()) { AppLog.e(T.PLUGINS, "An error occurred while removing the plugin with type: " + event.error.type + " and message: " + event.error.message); String toastMessage = getString(R.string.plugin_updated_failed_detailed, - mPlugin.getDisplayName(), event.error.message); + pluginDisplayName, event.error.message); ToastUtils.showToast(this, toastMessage, Duration.LONG); return; } @@ -1151,7 +1152,7 @@ public void onSitePluginDeleted(OnSitePluginDeleted event) { refreshViews(); invalidateOptionsMenu(); } - showSuccessfulPluginRemovedSnackbar(); + showSuccessfulPluginRemovedSnackbar(pluginDisplayName); } // This check should only handle events for already installed plugins - onSitePluginConfigured, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginUtils.java index 90de4fdc97da..4105ab07e513 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginUtils.java @@ -8,7 +8,6 @@ import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.fluxc.model.plugin.ImmutablePluginModel; import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.CrashLoggingUtils; import org.wordpress.android.util.SiteUtils; import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.helpers.Version; @@ -50,7 +49,6 @@ static boolean isUpdateAvailable(@Nullable ImmutablePluginModel immutablePlugin) String.format("An IllegalArgumentException occurred while trying to compare site plugin version: %s" + " with wporg plugin version: %s", installedVersionStr, availableVersionStr); AppLog.e(AppLog.T.PLUGINS, errorStr, e); - CrashLoggingUtils.logException(e, AppLog.T.PLUGINS, errorStr); // If the versions are not in the expected format, we can assume that an update is available if the version // values for the site plugin and wporg plugin are not the same return !installedVersionStr.equalsIgnoreCase(availableVersionStr); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/CriticalPostActionTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/CriticalPostActionTracker.kt index fd119f1e89e4..24b7b2992850 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/CriticalPostActionTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/CriticalPostActionTracker.kt @@ -4,7 +4,6 @@ import org.wordpress.android.BuildConfig import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T -import org.wordpress.android.util.CrashLoggingUtils class CriticalPostActionTracker( private val onStateChanged: () -> Unit, @@ -24,8 +23,6 @@ class CriticalPostActionTracker( AppLog.e(T.POSTS, errorMessage) if (shouldCrashOnUnexpectedAction) { throw IllegalStateException(errorMessage) - } else { - CrashLoggingUtils.log(errorMessage) } } map[localPostId] = criticalPostAction 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 67f27bb9a582..725e74bda2c2 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 @@ -139,6 +139,8 @@ import org.wordpress.android.ui.posts.editor.media.EditorMedia; import org.wordpress.android.ui.posts.editor.media.EditorMedia.AddExistingMediaSource; import org.wordpress.android.ui.posts.editor.media.EditorMediaListener; +import org.wordpress.android.ui.posts.prepublishing.PrepublishingBottomSheetListener; +import org.wordpress.android.ui.posts.prepublishing.home.usecases.PublishPostImmediatelyUseCase; import org.wordpress.android.ui.posts.reactnative.ReactNativeRequestHandler; import org.wordpress.android.ui.posts.services.AztecImageLoader; import org.wordpress.android.ui.posts.services.AztecVideoLoader; @@ -157,7 +159,6 @@ import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.AutolinkUtils; -import org.wordpress.android.util.CrashLoggingUtils; import org.wordpress.android.util.DateTimeUtilsWrapper; import org.wordpress.android.util.DisplayUtils; import org.wordpress.android.util.FluxCUtils; @@ -176,6 +177,7 @@ import org.wordpress.android.util.WPMediaUtils; import org.wordpress.android.util.WPPermissionUtils; import org.wordpress.android.util.WPUrlUtils; +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper; import org.wordpress.android.util.analytics.AnalyticsUtils; import org.wordpress.android.util.analytics.AnalyticsUtils.BlockEditorEnabledSource; import org.wordpress.android.util.helpers.MediaFile; @@ -223,11 +225,10 @@ public class EditPostActivity extends LocaleAwareActivity implements EditorPhotoPickerListener, EditorMediaListener, EditPostSettingsFragment.EditPostActivityHook, - BasicFragmentDialog.BasicDialogPositiveClickInterface, - BasicFragmentDialog.BasicDialogNegativeClickInterface, PostSettingsListDialogFragment.OnPostSettingsDialogFragmentListener, HistoryListFragment.HistoryItemClickInterface, EditPostSettingsCallback, + PrepublishingBottomSheetListener, PrivateAtCookieProgressDialogOnDismissListener, ExceptionLogger { public static final String ACTION_REBLOG = "reblogAction"; @@ -260,8 +261,6 @@ public class EditPostActivity extends LocaleAwareActivity implements private static final String STATE_KEY_REVISION = "stateKeyRevision"; private static final String STATE_KEY_EDITOR_SESSION_DATA = "stateKeyEditorSessionData"; private static final String STATE_KEY_GUTENBERG_IS_SHOWN = "stateKeyGutenbergIsShown"; - private static final String TAG_PUBLISH_CONFIRMATION_DIALOG = "tag_publish_confirmation_dialog"; - private static final String TAG_UPDATE_CONFIRMATION_DIALOG = "tag_update_confirmation_dialog"; private static final String TAG_GB_INFORMATIVE_DIALOG = "tag_gb_informative_dialog"; private static final String TAG_GB_ROLLOUT_V2_INFORMATIVE_DIALOG = "tag_gb_rollout_v2_informative_dialog"; @@ -353,6 +352,8 @@ enum RestartEditorOptions { @Inject protected PrivateAtomicCookie mPrivateAtomicCookie; @Inject ImageEditorTracker mImageEditorTracker; @Inject ReblogUtils mReblogUtils; + @Inject AnalyticsTrackerWrapper mAnalyticsTrackerWrapper; + @Inject PublishPostImmediatelyUseCase mPublishPostImmediatelyUseCase; private StorePostViewModel mViewModel; @@ -445,7 +446,7 @@ protected void onCreate(Bundle savedInstanceState) { FragmentManager fragmentManager = getSupportFragmentManager(); Bundle extras = getIntent().getExtras(); String action = getIntent().getAction(); - boolean isRestarting = !RestartEditorOptions.NO_RESTART.name().equals(extras.getString(EXTRA_RESTART_EDITOR)); + boolean isRestarting = checkToRestart(getIntent()); if (savedInstanceState == null) { if (!getIntent().hasExtra(EXTRA_POST_LOCAL_ID) || Intent.ACTION_SEND.equals(action) @@ -1260,8 +1261,6 @@ public boolean onOptionsItemSelected(final MenuItem item) { private void logWrongMenuState(String logMsg) { AppLog.w(T.EDITOR, logMsg); - // Lets record this event in Sentry - CrashLoggingUtils.logException(new IllegalStateException(logMsg), T.EDITOR); } private void showEmptyPostErrorForSecondaryAction() { @@ -1304,7 +1303,9 @@ private boolean performSecondaryAction() { uploadPost(false); return true; case PUBLISH_NOW: - showPublishConfirmationDialogAndPublishPost(); + mAnalyticsTrackerWrapper.track(Stat.EDITOR_POST_PUBLISH_TAPPED); + mPublishPostImmediatelyUseCase.updatePostToPublishImmediately(mEditPostRepository, mIsNewPost); + showPrepublishingNudgeBottomSheet(); return true; case NONE: throw new IllegalStateException("Switch in `secondaryAction` shouldn't go through the NONE case"); @@ -1397,43 +1398,17 @@ private void trackPostSessionEditorModeSwitch() { mHtmlModeMenuStateOn ? Editor.HTML : (isGutenberg ? Editor.GUTENBERG : Editor.CLASSIC)); } - private void showUpdateConfirmationDialogAndUploadPost() { - showConfirmationDialogAndUploadPost(TAG_UPDATE_CONFIRMATION_DIALOG, - getString(R.string.dialog_confirm_update_title), - mEditPostRepository.isPage() ? getString(R.string.dialog_confirm_update_message_page) - : getString(R.string.dialog_confirm_update_message_post), - getString(R.string.dialog_confirm_update_yes), - getString(R.string.keep_editing)); - } - - private void showPublishConfirmationDialogAndPublishPost() { - showConfirmationDialogAndUploadPost(TAG_PUBLISH_CONFIRMATION_DIALOG, - getString(R.string.dialog_confirm_publish_title), - mEditPostRepository.isPage() ? getString(R.string.dialog_confirm_publish_message_page) - : getString(R.string.dialog_confirm_publish_message_post), - getString(R.string.dialog_confirm_publish_yes), - getString(R.string.keep_editing)); - } - - private void showConfirmationDialogAndUploadPost(@NonNull String identifier, @NonNull String title, - @NonNull String description, @NonNull String positiveButton, - @NonNull String negativeButton) { - BasicFragmentDialog publishConfirmationDialog = new BasicFragmentDialog(); - publishConfirmationDialog.initialize(identifier, title, description, positiveButton, negativeButton, null); - publishConfirmationDialog.show(getSupportFragmentManager(), identifier); - } - private void performPrimaryAction() { switch (getPrimaryAction()) { - case UPDATE: - showUpdateConfirmationDialogAndUploadPost(); - return; case PUBLISH_NOW: - showPublishConfirmationDialogAndPublishPost(); + mAnalyticsTrackerWrapper.track(Stat.EDITOR_POST_PUBLISH_TAPPED); + showPrepublishingNudgeBottomSheet(); return; - // In other cases, we'll upload the post without changing its status + case UPDATE: case SCHEDULE: case SUBMIT_FOR_REVIEW: + showPrepublishingNudgeBottomSheet(); + return; case SAVE: uploadPost(false); break; @@ -1501,28 +1476,7 @@ private void onUploadSuccess(MediaModel media) { } } - private void onUploadError(MediaModel media, MediaError error) { - String localMediaId = String.valueOf(media.getId()); - Map properties = null; - MediaFile mf = FluxCUtils.mediaFileFromMediaModel(media); - if (mf != null) { - properties = AnalyticsUtils.getMediaProperties(this, mf.isVideo(), null, mf.getFilePath()); - properties.put("error_type", error.type.name()); - } - AnalyticsTracker.track(Stat.EDITOR_UPLOAD_MEDIA_FAILED, properties); - - // Display custom error depending on error type - String errorMessage = WPMediaUtils.getErrorMessage(this, media, error); - if (errorMessage == null) { - errorMessage = TextUtils.isEmpty(error.message) ? getString(R.string.tap_to_try_again) : error.message; - } - - if (mEditorMediaUploadListener != null) { - mEditorMediaUploadListener.onMediaUploadFailed(localMediaId, - EditorFragmentAbstract.getEditorMimeType(mf), errorMessage); - } - } private void onUploadProgress(MediaModel media, float progress) { String localMediaId = String.valueOf(media.getId()); @@ -1684,10 +1638,7 @@ private boolean isError8828(@NotNull Throwable throwable) { @Override public void log(@NotNull String s) { - // For now, we're wrapping up the actual log into an exception to reduce possibility - // of information not travelling to our Crash Logging Service. - // For more info: http://bit.ly/2oJHMG7 and http://bit.ly/2oPOtFX - CrashLoggingUtils.logException(new AztecEditorFragment.AztecLoggingException(s), T.EDITOR); + AppLog.e(T.EDITOR, s); } @Override @@ -1695,7 +1646,7 @@ public void logException(@NotNull Throwable throwable) { if (isError8828(throwable)) { return; } - CrashLoggingUtils.logException(new AztecEditorFragment.AztecLoggingException(throwable), T.EDITOR); + AppLog.e(T.EDITOR, throwable); } @Override @@ -1703,8 +1654,7 @@ public void logException(@NotNull Throwable throwable, String s) { if (isError8828(throwable)) { return; } - CrashLoggingUtils.logException( - new AztecEditorFragment.AztecLoggingException(throwable), T.EDITOR, s); + AppLog.e(T.EDITOR, s); } }); } @@ -1752,41 +1702,6 @@ public void onImageSettingsRequested(EditorImageMetaData editorImageMetaData) { ActivityLauncher.openImageEditor(this, inputData); } - @Override - public void onNegativeClicked(@NonNull String instanceTag) { - switch (instanceTag) { - case TAG_PUBLISH_CONFIRMATION_DIALOG: - case TAG_UPDATE_CONFIRMATION_DIALOG: - break; - default: - AppLog.e(T.EDITOR, "Dialog instanceTag is not recognized"); - throw new UnsupportedOperationException("Dialog instanceTag is not recognized"); - } - } - - @Override - public void onPositiveClicked(@NonNull String instanceTag) { - switch (instanceTag) { - case TAG_UPDATE_CONFIRMATION_DIALOG: - uploadPost(false); - break; - case TAG_PUBLISH_CONFIRMATION_DIALOG: - uploadPost(true); - AppRatingDialog.INSTANCE - .incrementInteractions(APP_REVIEWS_EVENT_INCREMENTED_BY_PUBLISHING_POST_OR_PAGE); - break; - case TAG_GB_INFORMATIVE_DIALOG: - // no op - break; - case TAG_GB_ROLLOUT_V2_INFORMATIVE_DIALOG: - // no op - break; - default: - AppLog.e(T.EDITOR, "Dialog instanceTag is not recognized"); - throw new UnsupportedOperationException("Dialog instanceTag is not recognized"); - } - } - /* * user clicked OK on a settings list dialog displayed from the settings fragment - pass the event * along to the settings fragment @@ -1860,6 +1775,27 @@ private void saveResult(boolean saved, boolean uploadNotStarted) { setResult(RESULT_OK, i); } + private void showPrepublishingNudgeBottomSheet() { + mViewPager.setCurrentItem(PAGE_CONTENT); + ActivityUtils.hideKeyboard(this); + + Fragment fragment = getSupportFragmentManager().findFragmentByTag( + PrepublishingBottomSheetFragment.TAG); + if (fragment == null) { + PrepublishingBottomSheetFragment prepublishingFragment = + PrepublishingBottomSheetFragment.newInstance(getSite(), mIsPage); + prepublishingFragment.show(getSupportFragmentManager(), PrepublishingBottomSheetFragment.TAG); + } + } + + @Override public void onSubmitButtonClicked(boolean publishPost) { + uploadPost(publishPost); + if (publishPost) { + AppRatingDialog.INSTANCE + .incrementInteractions(APP_REVIEWS_EVENT_INCREMENTED_BY_PUBLISHING_POST_OR_PAGE); + } + } + private void uploadPost(final boolean publishPost) { AccountModel account = mAccountStore.getAccount(); // prompt user to verify e-mail before publishing @@ -1908,7 +1844,13 @@ private void uploadPost(final boolean publishPost) { if (postModel.getStatus().equals(PostStatus.SCHEDULED.toString())) { postModel.setDateCreated(mDateTimeUtils.currentTimeInIso8601()); } - postModel.setStatus(PostStatus.PUBLISHED.toString()); + + if (mUploadUtilsWrapper.userCanPublish(getSite())) { + postModel.setStatus(PostStatus.PUBLISHED.toString()); + } else { + postModel.setStatus(PostStatus.PENDING.toString()); + } + mPostEditorAnalyticsSession.setOutcome(Outcome.PUBLISH); } else { mPostEditorAnalyticsSession.setOutcome(Outcome.SAVE); @@ -2024,6 +1966,8 @@ public Fragment getItem(int position) { String postType = mIsPage ? "page" : "post"; String languageString = LocaleManager.getLanguage(EditPostActivity.this); String wpcomLocaleSlug = languageString.replace("_", "-").toLowerCase(Locale.ENGLISH); + boolean supportsStockPhotos = mSite.isUsingWpComRestApi(); + boolean isWpCom = getSite().isWPCom(); boolean isSiteUsingWpComRestApi = mSite.isUsingWpComRestApi(); return GutenbergEditorFragment.newInstance( "", @@ -2031,6 +1975,13 @@ public Fragment getItem(int position) { postType, mIsNewPost, wpcomLocaleSlug, + supportsStockPhotos, + mSite.getUrl(), + !isWpCom, + mAccountStore.getAccount().getUserId(), + isWpCom ? mAccountStore.getAccount().getUserName() : mSite.getUsername(), + isWpCom ? "" : mSite.getPassword(), + mAccountStore.getAccessToken(), isSiteUsingWpComRestApi); } else { // If gutenberg editor is not selected, default to Aztec. @@ -2796,7 +2747,6 @@ public void onVideoPressInfoRequested(final String videoId) { @Override public Map onAuthHeaderRequested(String url) { Map authHeaders = new HashMap<>(); - String token = mAccountStore.getAccessToken(); if (mSite.isPrivate() && WPUrlUtils.safeToAddWordPressComAuthToken(url) && !TextUtils.isEmpty(token)) { @@ -2919,12 +2869,12 @@ public void onMediaUploaded(OnMediaUploaded event) { } if (event.isError()) { - onUploadError(event.media, event.error); + mEditorMedia.onMediaUploadError(mEditorMediaUploadListener, event.media, event.error); } else if (event.completed) { // if the remote url on completed is null, we consider this upload wasn't successful if (event.media.getUrl() == null) { MediaError error = new MediaError(MediaErrorType.GENERIC_ERROR); - onUploadError(event.media, error); + mEditorMedia.onMediaUploadError(mEditorMediaUploadListener, event.media, error); } else { onUploadSuccess(event.media); } @@ -3113,12 +3063,12 @@ public void syncPostObjectWithUiAndSaveIt(@Nullable OnPostUpdatedFromUIListener @Override public Consumer getExceptionLogger() { - return (Exception e) -> CrashLoggingUtils.logException(e, T.EDITOR); + return (Exception e) -> AppLog.e(T.EDITOR, e); } @Override public Consumer getBreadcrumbLogger() { - return CrashLoggingUtils::log; + return (String s) -> AppLog.e(T.EDITOR, s); } private void updateAddingMediaToEditorProgressDialogState(ProgressDialogUiState uiState) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPublishSettingsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPublishSettingsFragment.kt index ca4483bc2a9a..c96a1d744944 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPublishSettingsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPublishSettingsFragment.kt @@ -1,35 +1,16 @@ package org.wordpress.android.ui.posts -import android.app.AlarmManager -import android.app.PendingIntent -import android.content.Context.ALARM_SERVICE -import android.content.Intent import android.os.Bundle -import android.provider.CalendarContract -import android.provider.CalendarContract.Events -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.app.NotificationManagerCompat -import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import org.wordpress.android.R import org.wordpress.android.WordPress -import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel -import org.wordpress.android.ui.posts.EditPostSettingsFragment.EditPostActivityHook -import org.wordpress.android.util.AccessibilityUtils -import org.wordpress.android.util.ToastUtils -import org.wordpress.android.util.ToastUtils.Duration.SHORT -import javax.inject.Inject +import org.wordpress.android.ui.posts.PublishSettingsFragmentType.EDIT_POST -class EditPostPublishSettingsFragment : Fragment() { - @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - private lateinit var viewModel: EditPostPublishSettingsViewModel +class EditPostPublishSettingsFragment : PublishSettingsFragment() { + override fun getContentLayout() = R.layout.edit_post_published_settings_fragment + override fun getPublishSettingsFragmentType() = EDIT_POST + override fun setupContent(rootView: ViewGroup, viewModel: PublishSettingsViewModel) {} override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -38,159 +19,6 @@ class EditPostPublishSettingsFragment : Fragment() { .get(EditPostPublishSettingsViewModel::class.java) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val rootView = inflater.inflate(R.layout.edit_post_published_settings_fragment, container, false) as ViewGroup - val dateAndTime = rootView.findViewById(R.id.publish_time_and_date) - val dateAndTimeContainer = rootView.findViewById(R.id.publish_time_and_date_container) - val publishNotification = rootView.findViewById(R.id.publish_notification) - val publishNotificationTitle = rootView.findViewById(R.id.publish_notification_title) - val publishNotificationContainer = rootView.findViewById(R.id.publish_notification_container) - val addToCalendarContainer = rootView.findViewById(R.id.post_add_to_calendar_container) - val addToCalendar = rootView.findViewById(R.id.post_add_to_calendar) - - AccessibilityUtils.disableHintAnnouncement(dateAndTime) - AccessibilityUtils.disableHintAnnouncement(publishNotification) - - dateAndTimeContainer.setOnClickListener { showPostDateSelectionDialog() } - - viewModel.onDatePicked.observe(viewLifecycleOwner, Observer { - it?.applyIfNotHandled { - showPostTimeSelectionDialog() - } - }) - viewModel.onPublishedDateChanged.observe(viewLifecycleOwner, Observer { - it?.let { date -> - viewModel.updatePost(date, getPostRepository()) - } - }) - viewModel.onNotificationTime.observe(viewLifecycleOwner, Observer { - it?.let { notificationTime -> - getPostRepository()?.let { postRepository -> - viewModel.scheduleNotification(postRepository, notificationTime) - } - } - }) - viewModel.onUiModel.observe(viewLifecycleOwner, Observer { - it?.let { uiModel -> - dateAndTime.text = uiModel.publishDateLabel - publishNotificationTitle.isEnabled = uiModel.notificationEnabled - publishNotification.isEnabled = uiModel.notificationEnabled - publishNotificationContainer.isEnabled = uiModel.notificationEnabled - addToCalendar.isEnabled = uiModel.notificationEnabled - addToCalendarContainer.isEnabled = uiModel.notificationEnabled - if (uiModel.notificationEnabled) { - publishNotificationContainer.setOnClickListener { - getPostRepository()?.getPost()?.let { postModel -> - viewModel.onShowDialog(postModel) - } - } - addToCalendarContainer.setOnClickListener { - getPostRepository()?.let { postRepository -> - viewModel.onAddToCalendar(postRepository) - } - } - } else { - publishNotificationContainer.setOnClickListener(null) - addToCalendarContainer.setOnClickListener(null) - } - publishNotification.setText(uiModel.notificationLabel) - publishNotificationContainer.visibility = if (uiModel.notificationVisible) View.VISIBLE else View.GONE - addToCalendarContainer.visibility = if (uiModel.notificationVisible) View.VISIBLE else View.GONE - } - }) - viewModel.onShowNotificationDialog.observe(viewLifecycleOwner, Observer { - it?.getContentIfNotHandled()?.let { notificationTime -> - showNotificationTimeSelectionDialog(notificationTime) - } - }) - viewModel.onToast.observe(viewLifecycleOwner, Observer { - it?.applyIfNotHandled { - ToastUtils.showToast( - context, - this, - SHORT, - Gravity.TOP - ) - } - }) - viewModel.onNotificationAdded.observe(viewLifecycleOwner, Observer { event -> - event?.getContentIfNotHandled()?.let { notification -> - activity?.let { - NotificationManagerCompat.from(it).cancel(notification.id) - val notificationIntent = Intent(it, PublishNotificationReceiver::class.java) - notificationIntent.putExtra(PublishNotificationReceiver.NOTIFICATION_ID, notification.id) - val pendingIntent = PendingIntent.getBroadcast( - it, - notification.id, - notificationIntent, - PendingIntent.FLAG_CANCEL_CURRENT - ) - - val alarmManager = it.getSystemService(ALARM_SERVICE) as AlarmManager - alarmManager.set( - AlarmManager.RTC_WAKEUP, - notification.scheduledTime, - pendingIntent - ) - } - } - }) - viewModel.onAddToCalendar.observe(viewLifecycleOwner, Observer { - it?.getContentIfNotHandled()?.let { calendarEvent -> - val calIntent = Intent(Intent.ACTION_INSERT) - calIntent.data = Events.CONTENT_URI - calIntent.type = "vnd.android.cursor.item/event" - calIntent.putExtra(Events.TITLE, calendarEvent.title) - calIntent.putExtra(Events.DESCRIPTION, calendarEvent.description) - calIntent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, calendarEvent.startTime) - startActivity(calIntent) - } - }) - viewModel.start(getPostRepository()) - return rootView - } - - private fun showPostDateSelectionDialog() { - if (!isAdded) { - return - } - - val fragment = PostDatePickerDialogFragment.newInstance() - fragment.show(requireActivity().supportFragmentManager, PostDatePickerDialogFragment.TAG) - } - - private fun showPostTimeSelectionDialog() { - if (!isAdded) { - return - } - - val fragment = PostTimePickerDialogFragment.newInstance() - fragment.show(requireActivity().supportFragmentManager, PostTimePickerDialogFragment.TAG) - } - - private fun showNotificationTimeSelectionDialog(schedulingReminderPeriod: SchedulingReminderModel.Period?) { - if (!isAdded) { - return - } - - val fragment = PostNotificationScheduleTimeDialogFragment.newInstance(schedulingReminderPeriod) - fragment.show(requireActivity().supportFragmentManager, PostNotificationScheduleTimeDialogFragment.TAG) - } - - private fun getPostRepository(): EditPostRepository? { - return getEditPostActivityHook()?.editPostRepository - } - - private fun getEditPostActivityHook(): EditPostActivityHook? { - val activity = activity ?: return null - - return if (activity is EditPostActivityHook) { - activity - } else { - throw RuntimeException("$activity must implement EditPostActivityHook") - } - } - companion object { fun newInstance() = EditPostPublishSettingsFragment() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPublishSettingsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPublishSettingsViewModel.kt index da96af4d0a92..77c16d487506 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPublishSettingsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostPublishSettingsViewModel.kt @@ -1,271 +1,21 @@ package org.wordpress.android.ui.posts -import android.text.TextUtils -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.wordpress.android.R -import org.wordpress.android.fluxc.model.PostImmutableModel -import org.wordpress.android.fluxc.model.post.PostStatus -import org.wordpress.android.fluxc.model.post.PostStatus.DRAFT -import org.wordpress.android.fluxc.model.post.PostStatus.PUBLISHED -import org.wordpress.android.fluxc.model.post.PostStatus.SCHEDULED import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore -import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period -import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.OFF -import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.ONE_HOUR -import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.TEN_MINUTES -import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.WHEN_PUBLISHED import org.wordpress.android.fluxc.store.SiteStore -import org.wordpress.android.ui.posts.EditPostRepository.UpdatePostResult -import org.wordpress.android.util.DateTimeUtils import org.wordpress.android.util.LocaleManagerWrapper -import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ResourceProvider -import java.util.Calendar import javax.inject.Inject -class EditPostPublishSettingsViewModel -@Inject constructor( - private val resourceProvider: ResourceProvider, - private val postSettingsUtils: PostSettingsUtils, - private val localeManagerWrapper: LocaleManagerWrapper, - private val postSchedulingNotificationStore: PostSchedulingNotificationStore, - private val siteStore: SiteStore -) : ViewModel() { - var canPublishImmediately: Boolean = false - - var year: Int? = null - private set - var month: Int? = null - private set - var day: Int? = null - private set - var hour: Int? = null - private set - var minute: Int? = null - private set - - private val _onDatePicked = MutableLiveData>() - val onDatePicked: LiveData> = _onDatePicked - private val _onPublishedDateChanged = MutableLiveData() - val onPublishedDateChanged: LiveData = _onPublishedDateChanged - private val _onPostStatusChanged = MutableLiveData() - val onPostStatusChanged: LiveData = _onPostStatusChanged - private val _onUiModel = MutableLiveData() - val onUiModel: LiveData = _onUiModel - private val _onToast = MutableLiveData>() - val onToast: LiveData> = _onToast - private val _onShowNotificationDialog = MutableLiveData>() - val onShowNotificationDialog: LiveData> = _onShowNotificationDialog - private val _onNotificationTime = MutableLiveData() - val onNotificationTime: LiveData = _onNotificationTime - private val _onNotificationAdded = MutableLiveData>() - val onNotificationAdded: LiveData> = _onNotificationAdded - private val _onAddToCalendar = MutableLiveData>() - val onAddToCalendar: LiveData> = _onAddToCalendar - - fun start(postRepository: EditPostRepository?) { - val startCalendar = postRepository?.let { getCurrentPublishDateAsCalendar(it) } - ?: localeManagerWrapper.getCurrentCalendar() - updateDateAndTimeFromCalendar(startCalendar) - onPostStatusChanged(postRepository?.getPost()) - } - - fun onPostStatusChanged(postModel: PostImmutableModel?) { - canPublishImmediately = postModel?.let { - PostUtils.shouldPublishImmediatelyOptionBeAvailable( - it.status - ) - } ?: false - updateUiModel(postModel = postModel) - } - - fun publishNow() { - val currentCalendar = localeManagerWrapper.getCurrentCalendar() - updateDateAndTimeFromCalendar(currentCalendar) - _onPublishedDateChanged.postValue(currentCalendar) - } - - fun onTimeSelected(selectedHour: Int, selectedMinute: Int) { - this.hour = selectedHour - this.minute = selectedMinute - val calendar = localeManagerWrapper.getCurrentCalendar() - calendar.set(year!!, month!!, day!!, hour!!, minute!!) - _onPublishedDateChanged.postValue(calendar) - } - - fun onDateSelected(year: Int, month: Int, dayOfMonth: Int) { - this.year = year - this.month = month - this.day = dayOfMonth - _onDatePicked.postValue(Event(Unit)) - } - - fun onShowDialog(postModel: PostImmutableModel) { - if (areNotificationsEnabled(postModel)) { - val currentPeriod = postSchedulingNotificationStore.getSchedulingReminderPeriod(postModel.id) - _onShowNotificationDialog.postValue(Event(currentPeriod)) - } else { - _onToast.postValue(Event(resourceProvider.getString(R.string.post_notification_error))) - } - } - - fun updatePost(updatedDate: Calendar, postRepository: EditPostRepository?) { - postRepository?.updateAsync({ postModel -> - val dateCreated = DateTimeUtils.iso8601FromDate(updatedDate.time) - postModel.setDateCreated(dateCreated) - val initialPostStatus = postRepository.status - val isPublishDateInTheFuture = PostUtils.isPublishDateInTheFuture(dateCreated) - var finalPostStatus = initialPostStatus - if (initialPostStatus == DRAFT && isPublishDateInTheFuture) { - // The previous logic was setting the status twice, once from draft to published and when the user - // picked the time, it set it from published to scheduled. This is now done in one step. - finalPostStatus = SCHEDULED - } else if (initialPostStatus == PUBLISHED && postRepository.isLocalDraft) { - // if user was changing dates for a local draft (not saved yet), only way to have it set to PUBLISH - // is by running into the if case above. So, if they're updating the date again by calling - // `updatePublishDate()`, get it back to DRAFT. - finalPostStatus = DRAFT - } else if (initialPostStatus == SCHEDULED && !isPublishDateInTheFuture) { - // if this is a SCHEDULED post and the user is trying to Back-date it now, let's update it to DRAFT. - // The other option was to make it published immediately but, let the user actively do that rather than - // having the app be smart about it - we don't want to accidentally publish a post. - finalPostStatus = DRAFT - // show toast only once, when time is shown - _onToast.postValue(Event(resourceProvider.getString(R.string.editor_post_converted_back_to_draft))) - } - postModel.setStatus(finalPostStatus.toString()) - _onPostStatusChanged.postValue(finalPostStatus) - val scheduledTime = postSchedulingNotificationStore.getSchedulingReminderPeriod(postRepository.id) - updateNotifications(postRepository, scheduledTime) - true - }, onCompleted = { postModel, result -> - if (result == UpdatePostResult.Updated) { - updateUiModel(postModel = postModel) - } - }) - } - - fun updateUiModel(postModel: PostImmutableModel?) { - if (postModel != null) { - val notificationTime = postSchedulingNotificationStore.getSchedulingReminderPeriod(postModel.id) - val publishDateLabel = postSettingsUtils.getPublishDateLabel(postModel) - val now = localeManagerWrapper.getCurrentCalendar().timeInMillis - 10000 - val dateCreated = (DateTimeUtils.dateFromIso8601(postModel.dateCreated) - ?: localeManagerWrapper.getCurrentCalendar().time).time - val enableNotification = areNotificationsEnabled(postModel) - val showNotification = dateCreated > now - val notificationLabel = if (enableNotification && showNotification) { - notificationTime.toLabel() - } else { - R.string.post_notification_off - } - _onUiModel.value = PublishUiModel( - publishDateLabel, - notificationLabel = notificationLabel, - notificationEnabled = enableNotification, - notificationVisible = showNotification - ) - } else { - _onUiModel.value = PublishUiModel(resourceProvider.getString(R.string.immediately)) - } - } - - fun onNotificationCreated(scheduleTime: Period?) { - _onNotificationTime.value = scheduleTime - } - - fun scheduleNotification(postRepository: EditPostRepository, notificationTime: Period) { - updateNotifications(postRepository, notificationTime) - updateUiModel(postRepository.getPost()) - } - - fun onAddToCalendar(postRepository: EditPostRepository) { - val startTime = DateTimeUtils.dateFromIso8601(postRepository.dateCreated).time - val site = siteStore.getSiteByLocalId(postRepository.localSiteId) - val title = resourceProvider.getString( - R.string.calendar_scheduled_post_title, - postRepository.title - ) - val description = resourceProvider.getString( - R.string.calendar_scheduled_post_description, - postRepository.title, - site.name ?: site.url, - postRepository.link - ) - _onAddToCalendar.value = Event(CalendarEvent(title, description, startTime)) - } - - private fun getCurrentPublishDateAsCalendar(postRepository: EditPostRepository): Calendar { - val calendar = localeManagerWrapper.getCurrentCalendar() - val dateCreated = postRepository.dateCreated - // Set the currently selected time if available - if (!TextUtils.isEmpty(dateCreated)) { - calendar.time = DateTimeUtils.dateFromIso8601(dateCreated) - calendar.timeZone = localeManagerWrapper.getTimeZone() - } - return calendar - } - - private fun updateNotifications( - postRepository: EditPostRepository, - schedulingReminderPeriod: Period = OFF - ) { - postSchedulingNotificationStore.deleteSchedulingReminders(postRepository.id) - if (schedulingReminderPeriod != OFF) { - val notificationId = postSchedulingNotificationStore.schedule(postRepository.id, schedulingReminderPeriod) - val scheduledCalendar = localeManagerWrapper.getCurrentCalendar().apply { - timeInMillis = System.currentTimeMillis() - time = DateTimeUtils.dateFromIso8601(postRepository.dateCreated) - val scheduledMinutes = when (schedulingReminderPeriod) { - ONE_HOUR -> -60 - TEN_MINUTES -> -10 - WHEN_PUBLISHED -> 0 - OFF -> return - } - add(Calendar.MINUTE, scheduledMinutes) - } - if (scheduledCalendar.after(localeManagerWrapper.getCurrentCalendar())) { - notificationId?.let { - _onNotificationAdded.postValue(Event(Notification(notificationId, scheduledCalendar.timeInMillis))) - } - } - } - } - - private fun areNotificationsEnabled(postModel: PostImmutableModel): Boolean { - val futureTime = localeManagerWrapper.getCurrentCalendar().timeInMillis + 6000 - val dateCreated = (DateTimeUtils.dateFromIso8601(postModel.dateCreated) - ?: localeManagerWrapper.getCurrentCalendar().time).time - return dateCreated > futureTime - } - - private fun Period.toLabel(): Int { - return when (this) { - OFF -> R.string.post_notification_off - ONE_HOUR -> R.string.post_notification_one_hour_before - TEN_MINUTES -> R.string.post_notification_ten_minutes_before - WHEN_PUBLISHED -> R.string.post_notification_when_published - } - } - - private fun updateDateAndTimeFromCalendar(startCalendar: Calendar) { - year = startCalendar.get(Calendar.YEAR) - month = startCalendar.get(Calendar.MONTH) - day = startCalendar.get(Calendar.DAY_OF_MONTH) - hour = startCalendar.get(Calendar.HOUR_OF_DAY) - minute = startCalendar.get(Calendar.MINUTE) - } - - data class PublishUiModel( - val publishDateLabel: String, - val notificationLabel: Int = R.string.post_notification_off, - val notificationEnabled: Boolean = false, - val notificationVisible: Boolean = true - ) - - data class Notification(val id: Int, val scheduledTime: Long) - - data class CalendarEvent(val title: String, val description: String, val startTime: Long) -} +class EditPostPublishSettingsViewModel @Inject constructor( + resourceProvider: ResourceProvider, + postSettingsUtils: PostSettingsUtils, + localeManagerWrapper: LocaleManagerWrapper, + postSchedulingNotificationStore: PostSchedulingNotificationStore, + siteStore: SiteStore +) : PublishSettingsViewModel( + resourceProvider, + postSettingsUtils, + localeManagerWrapper, + postSchedulingNotificationStore, + siteStore +) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostRepository.kt index a525ee4a519d..55b363f36e4d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostRepository.kt @@ -24,7 +24,6 @@ import org.wordpress.android.ui.posts.EditPostRepository.UpdatePostResult.Update import org.wordpress.android.ui.uploads.UploadService import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T -import org.wordpress.android.util.CrashLoggingUtils import org.wordpress.android.util.DateTimeUtils import org.wordpress.android.util.LocaleManagerWrapper import org.wordpress.android.viewmodel.Event @@ -149,7 +148,6 @@ class EditPostRepository Thread.currentThread().stackTrace )}" AppLog.e(T.EDITOR, message) - CrashLoggingUtils.log(message) } locked = lock } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java index 197641bcd210..7aa6f8d4cf9e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java @@ -44,6 +44,7 @@ import org.greenrobot.eventbus.ThreadMode; import org.wordpress.android.R; import org.wordpress.android.WordPress; +import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.fluxc.Dispatcher; import org.wordpress.android.fluxc.action.TaxonomyAction; import org.wordpress.android.fluxc.generated.SiteActionBuilder; @@ -63,12 +64,13 @@ import org.wordpress.android.ui.ActivityLauncher; import org.wordpress.android.ui.RequestCodes; import org.wordpress.android.ui.media.MediaBrowserType; -import org.wordpress.android.ui.posts.EditPostPublishSettingsViewModel.PublishUiModel; import org.wordpress.android.ui.posts.EditPostRepository.UpdatePostResult; import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageData; import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState; import org.wordpress.android.ui.posts.FeaturedImageHelper.TrackableEvent; import org.wordpress.android.ui.posts.PostSettingsListDialogFragment.DialogType; +import org.wordpress.android.ui.posts.PublishSettingsViewModel.PublishUiModel; +import org.wordpress.android.ui.posts.prepublishing.visibility.usecases.UpdatePostStatusUseCase; import org.wordpress.android.ui.prefs.SiteSettingsInterface; import org.wordpress.android.ui.prefs.SiteSettingsInterface.SiteSettingsListener; import org.wordpress.android.ui.utils.UiHelpers; @@ -79,6 +81,7 @@ import org.wordpress.android.util.GeocoderUtils; import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.util.image.ImageManager.RequestListener; import org.wordpress.android.util.image.ImageType; @@ -90,6 +93,7 @@ import java.util.Calendar; import java.util.Iterator; import java.util.List; +import java.util.Objects; import javax.inject.Inject; @@ -116,6 +120,7 @@ public class EditPostSettingsFragment extends Fragment { private LinearLayout mExcerptContainer; private LinearLayout mFormatContainer; private LinearLayout mTagsContainer; + private LinearLayout mPublishDateContainer; private TextView mExcerptTextView; private TextView mSlugTextView; private TextView mLocationTextView; @@ -125,6 +130,7 @@ public class EditPostSettingsFragment extends Fragment { private TextView mPostFormatTextView; private TextView mPasswordTextView; private TextView mPublishDateTextView; + private TextView mPublishDateTitleTextView; private TextView mCategoriesTagsHeaderTextView; private TextView mFeaturedImageHeaderTextView; private TextView mMoreOptionsHeaderTextView; @@ -149,12 +155,14 @@ public class EditPostSettingsFragment extends Fragment { @Inject FeaturedImageHelper mFeaturedImageHelper; @Inject UiHelpers mUiHelpers; @Inject PostSettingsUtils mPostSettingsUtils; + @Inject AnalyticsTrackerWrapper mAnalyticsTrackerWrapper; + @Inject UpdatePostStatusUseCase mUpdatePostStatusUseCase; @Inject ViewModelProvider.Factory mViewModelFactory; private EditPostPublishSettingsViewModel mPublishedViewModel; - interface EditPostActivityHook { + public interface EditPostActivityHook { EditPostRepository getEditPostRepository(); SiteModel getSite(); @@ -242,6 +250,9 @@ public void onCredentialsValidated(Exception error) { @Override public void onDestroy() { + if (mSiteSettings != null) { + mSiteSettings.clear(); + } mDispatcher.unregister(this); super.onDestroy(); } @@ -264,10 +275,12 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c mPostFormatTextView = rootView.findViewById(R.id.post_format); mPasswordTextView = rootView.findViewById(R.id.post_password); mPublishDateTextView = rootView.findViewById(R.id.publish_date); + mPublishDateTitleTextView = rootView.findViewById(R.id.publish_date_title); mCategoriesTagsHeaderTextView = rootView.findViewById(R.id.post_settings_categories_and_tags_header); mMoreOptionsHeaderTextView = rootView.findViewById(R.id.post_settings_more_options_header); mFeaturedImageHeaderTextView = rootView.findViewById(R.id.post_settings_featured_image_header); mPublishHeaderTextView = rootView.findViewById(R.id.post_settings_publish); + mPublishDateContainer = rootView.findViewById(R.id.publish_date_container); mFeaturedImageView = rootView.findViewById(R.id.post_featured_image); mLocalFeaturedImageView = rootView.findViewById(R.id.post_featured_image_local); @@ -363,8 +376,7 @@ public void onClick(View view) { } }); - final LinearLayout publishDateContainer = rootView.findViewById(R.id.publish_date_container); - publishDateContainer.setOnClickListener(new View.OnClickListener() { + mPublishDateContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { FragmentActivity activity = getActivity(); @@ -385,7 +397,8 @@ public void onClick(View view) { mPublishedViewModel.getOnUiModel().observe(getViewLifecycleOwner(), new Observer() { @Override public void onChanged(PublishUiModel uiModel) { - updatePublishDateTextView(uiModel.getPublishDateLabel()); + updatePublishDateTextView(uiModel.getPublishDateLabel(), + Objects.requireNonNull(getEditPostRepository().getPost())); } }); mPublishedViewModel.getOnPostStatusChanged().observe(getViewLifecycleOwner(), new Observer() { @@ -504,6 +517,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { case ACTIVITY_REQUEST_CODE_PICK_LOCATION: if (isAdded() && resultCode == RESULT_OK) { Place place = PlacePicker.getPlace(getActivity(), data); + mAnalyticsTrackerWrapper.track(Stat.EDITOR_POST_LOCATION_CHANGED); setLocation(place); } break; @@ -512,6 +526,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { if (extras != null && extras.containsKey(KEY_SELECTED_CATEGORY_IDS)) { @SuppressWarnings("unchecked") List categoryList = (ArrayList) extras.getSerializable(KEY_SELECTED_CATEGORY_IDS); + mAnalyticsTrackerWrapper.track(Stat.EDITOR_POST_CATEGORIES_ADDED); updateCategories(categoryList); } break; @@ -519,6 +534,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { extras = data.getExtras(); if (resultCode == RESULT_OK && extras != null) { String selectedTags = extras.getString(PostSettingsTagsActivity.KEY_SELECTED_TAGS); + PostAnalyticsUtilsKt.trackPostSettings(mAnalyticsTrackerWrapper, Stat.EDITOR_POST_TAGS_CHANGED); updateTags(selectedTags); } break; @@ -537,6 +553,7 @@ private void showPostExcerptDialog() { new PostSettingsInputDialogFragment.PostSettingsInputDialogListener() { @Override public void onInputUpdated(String input) { + mAnalyticsTrackerWrapper.track(Stat.EDITOR_POST_EXCERPT_CHANGED); updateExcerpt(input); } }); @@ -554,6 +571,7 @@ private void showSlugDialog() { new PostSettingsInputDialogFragment.PostSettingsInputDialogListener() { @Override public void onInputUpdated(String input) { + mAnalyticsTrackerWrapper.track(Stat.EDITOR_POST_SLUG_CHANGED); updateSlug(input); } }); @@ -594,10 +612,12 @@ public void onPostSettingsFragmentPositiveButtonClicked(@NonNull PostSettingsLis int index = fragment.getCheckedIndex(); PostStatus status = getPostStatusAtIndex(index); updatePostStatus(status); + PostAnalyticsUtilsKt.trackPostSettings(mAnalyticsTrackerWrapper, Stat.EDITOR_POST_VISIBILITY_CHANGED); break; case POST_FORMAT: String formatName = fragment.getSelectedItem(); updatePostFormat(getPostFormatKeyFromName(formatName)); + mAnalyticsTrackerWrapper.track(Stat.EDITOR_POST_FORMAT_CHANGED); break; } } @@ -647,6 +667,8 @@ private void showPostPasswordDialog() { new PostSettingsInputDialogFragment.PostSettingsInputDialogListener() { @Override public void onInputUpdated(String input) { + PostAnalyticsUtilsKt + .trackPostSettings(mAnalyticsTrackerWrapper, Stat.EDITOR_POST_PASSWORD_CHANGED); updatePassword(input); } }); @@ -753,19 +775,15 @@ private void updateCategories(List categoryList) { } } - public void updatePostStatus(PostStatus postStatus) { + void updatePostStatus(PostStatus postStatus) { EditPostRepository editPostRepository = getEditPostRepository(); if (editPostRepository != null) { - editPostRepository.updateAsync(postModel -> { - postModel.setStatus(postStatus.toString()); - return true; - }, (postModel, result) -> { - if (result == UpdatePostResult.Updated.INSTANCE) { - updatePostStatusRelatedViews(postModel); - updateSaveButton(); - } - return null; - }); + mUpdatePostStatusUseCase.updatePostStatus(postStatus, editPostRepository, + postImmutableModel -> { + updatePostStatusRelatedViews(postImmutableModel); + updateSaveButton(); + return null; + }); } } @@ -844,12 +862,18 @@ private void updatePublishDateTextView(PostImmutableModel postModel) { } if (postModel != null) { String labelToUse = mPostSettingsUtils.getPublishDateLabel(postModel); - mPublishDateTextView.setText(labelToUse); + updatePublishDateTextView(labelToUse, postModel); } } - private void updatePublishDateTextView(String label) { + private void updatePublishDateTextView(String label, PostImmutableModel postImmutableModel) { mPublishDateTextView.setText(label); + + boolean isPrivatePost = postImmutableModel.getStatus().equals(PostStatus.PRIVATE.toString()); + + mPublishDateTextView.setEnabled(!isPrivatePost); + mPublishDateTitleTextView.setEnabled(!isPrivatePost); + mPublishDateContainer.setEnabled(!isPrivatePost); } private void updateCategoriesTextView(PostImmutableModel post) { @@ -1185,6 +1209,7 @@ public boolean onMenuItemClick(MenuItem menuItem) { if (menuItem.getItemId() == R.id.menu_change_location) { showLocationPicker(); } else if (menuItem.getItemId() == R.id.menu_remove_location) { + mAnalyticsTrackerWrapper.track(Stat.EDITOR_POST_LOCATION_CHANGED); setLocation(null); } return true; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GetPostTagsUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GetPostTagsUseCase.kt new file mode 100644 index 000000000000..ab5e19b54bbd --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GetPostTagsUseCase.kt @@ -0,0 +1,25 @@ +package org.wordpress.android.ui.posts + +import android.text.TextUtils +import dagger.Reusable +import org.apache.commons.text.StringEscapeUtils +import javax.inject.Inject + +@Reusable +class GetPostTagsUseCase @Inject constructor() { + fun getTags(editPostRepository: EditPostRepository): String? { + val tags = editPostRepository.getPost()?.tagNameList + return tags?.let { + if (it.isNotEmpty()) { + formatTags(it) + } else { + null + } + } + } + + private fun formatTags(tags: List): String { + val formattedTags = TextUtils.join(",", tags) + return StringEscapeUtils.unescapeHtml4(formattedTags) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt index 5d79752317f0..32f6be2840c9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt @@ -41,6 +41,7 @@ import org.wordpress.android.widgets.PostListButtonType.BUTTON_MOVE_TO_DRAFT import org.wordpress.android.widgets.PostListButtonType.BUTTON_PREVIEW import org.wordpress.android.widgets.PostListButtonType.BUTTON_PUBLISH import org.wordpress.android.widgets.PostListButtonType.BUTTON_RETRY +import org.wordpress.android.widgets.PostListButtonType.BUTTON_SHOW_MOVE_TRASHED_POST_TO_DRAFT_DIALOG import org.wordpress.android.widgets.PostListButtonType.BUTTON_STATS import org.wordpress.android.widgets.PostListButtonType.BUTTON_SUBMIT import org.wordpress.android.widgets.PostListButtonType.BUTTON_SYNC @@ -60,6 +61,7 @@ class PostActionHandler( private val hasUnhandledAutoSave: (PostModel) -> Boolean, private val triggerPostListAction: (PostListAction) -> Unit, private val triggerPostUploadAction: (PostUploadAction) -> Unit, + private val triggerPublishAction: (PostModel) -> Unit, private val invalidateList: () -> Unit, private val checkNetworkConnection: () -> Boolean, private val showSnackbar: (SnackbarMessageHolder) -> Unit, @@ -78,7 +80,7 @@ class PostActionHandler( moveTrashedPostToDraft(post) } BUTTON_PUBLISH -> { - postListDialogHelper.showPublishConfirmationDialog(post) + triggerPublishAction.invoke(post) } BUTTON_SYNC -> { postListDialogHelper.showSyncScheduledPostConfirmationDialog(post) @@ -111,6 +113,9 @@ class PostActionHandler( BUTTON_CANCEL_PENDING_AUTO_UPLOAD -> { cancelPendingAutoUpload(post) } + BUTTON_SHOW_MOVE_TRASHED_POST_TO_DRAFT_DIALOG -> { + postListDialogHelper.showMoveTrashedPostToDraftDialog(post) + } BUTTON_MORE -> { } // do nothing - ui will show a popup window } @@ -150,6 +155,17 @@ class PostActionHandler( } } + fun publishPost(post: PostModel) { + triggerPostUploadAction.invoke(PublishPost(dispatcher, site, post)) + } + + fun moveTrashedPostToDraft(localPostId: Int) { + val post = postStore.getPostByLocalPostId(localPostId) + if (post != null) { + moveTrashedPostToDraft(post) + } + } + private fun moveTrashedPostToDraft(post: PostModel) { /* * We need network connection to move a post to remote draft. We can technically move it to the local drafts diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostAnalyticsUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostAnalyticsUtils.kt new file mode 100644 index 000000000000..376158fe3c5a --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostAnalyticsUtils.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.ui.posts + +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper + +private const val VIA = "via" +private const val POST_SETTINGS = "settings" +private const val PREPUBLISHING_NUDGES = "prepublishing_nudges" + +fun AnalyticsTrackerWrapper.trackPrepublishingNudges(stat: Stat) { + this.track(stat, mapOf(VIA to PREPUBLISHING_NUDGES)) +} + +fun AnalyticsTrackerWrapper.trackPostSettings(stat: Stat) { + this.track(stat, mapOf(VIA to POST_SETTINGS)) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostDatePickerDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostDatePickerDialogFragment.kt index 158d77be7cdc..fbf6871b0239 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostDatePickerDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostDatePickerDialogFragment.kt @@ -10,17 +10,29 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import org.wordpress.android.R import org.wordpress.android.WordPress +import org.wordpress.android.ui.posts.PublishSettingsFragmentType.EDIT_POST +import org.wordpress.android.ui.posts.PublishSettingsFragmentType.PREPUBLISHING_NUDGES +import org.wordpress.android.ui.posts.prepublishing.PrepublishingPublishSettingsViewModel import javax.inject.Inject class PostDatePickerDialogFragment : DialogFragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - private lateinit var viewModel: EditPostPublishSettingsViewModel + private lateinit var viewModel: PublishSettingsViewModel override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) - .get(EditPostPublishSettingsViewModel::class.java) + val publishSettingsFragmentType = arguments?.getParcelable( + ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE + ) + + when (publishSettingsFragmentType) { + EDIT_POST -> viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) + .get(EditPostPublishSettingsViewModel::class.java) + PREPUBLISHING_NUDGES -> viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) + .get(PrepublishingPublishSettingsViewModel::class.java) + } - val datePickerDialog = DatePickerDialog(activity, + val datePickerDialog = DatePickerDialog( + activity, null, viewModel.year ?: 0, viewModel.month ?: 0, @@ -56,9 +68,17 @@ class PostDatePickerDialogFragment : DialogFragment() { companion object { const val TAG = "post_date_picker_dialog_fragment" + private const val ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE = "publish_settings_fragment_type" - fun newInstance(): PostDatePickerDialogFragment { - return PostDatePickerDialogFragment() + fun newInstance(publishSettingsFragmentType: PublishSettingsFragmentType): PostDatePickerDialogFragment { + return PostDatePickerDialogFragment().apply { + arguments = Bundle().apply { + putParcelable( + ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE, + publishSettingsFragmentType + ) + } + } } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListActionTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListActionTracker.kt index fb8c78673ee7..3002502577a9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListActionTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListActionTracker.kt @@ -14,6 +14,7 @@ import org.wordpress.android.widgets.PostListButtonType.BUTTON_MOVE_TO_DRAFT import org.wordpress.android.widgets.PostListButtonType.BUTTON_PREVIEW import org.wordpress.android.widgets.PostListButtonType.BUTTON_PUBLISH import org.wordpress.android.widgets.PostListButtonType.BUTTON_RETRY +import org.wordpress.android.widgets.PostListButtonType.BUTTON_SHOW_MOVE_TRASHED_POST_TO_DRAFT_DIALOG import org.wordpress.android.widgets.PostListButtonType.BUTTON_STATS import org.wordpress.android.widgets.PostListButtonType.BUTTON_SUBMIT import org.wordpress.android.widgets.PostListButtonType.BUTTON_SYNC @@ -45,6 +46,7 @@ fun trackPostListAction(site: SiteModel, buttonType: PostListButtonType, postDat BUTTON_MORE -> "more" BUTTON_MOVE_TO_DRAFT -> "move_to_draft" BUTTON_CANCEL_PENDING_AUTO_UPLOAD -> "cancel_pending_auto_upload" + BUTTON_SHOW_MOVE_TRASHED_POST_TO_DRAFT_DIALOG -> "show_move_trashed_post_to_draft_post_dialog" } AnalyticsUtils.trackWithSiteDetails(statsEvent, site, properties) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt index 6ef8f2667d1c..64650fb3caea 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt @@ -11,7 +11,7 @@ import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.viewmodel.helpers.DialogHolder private const val CONFIRM_DELETE_POST_DIALOG_TAG = "CONFIRM_DELETE_POST_DIALOG_TAG" -private const val CONFIRM_PUBLISH_POST_DIALOG_TAG = "CONFIRM_PUBLISH_POST_DIALOG_TAG" +private const val CONFIRM_RESTORE_TRASHED_POST_DIALOG_TAG = "CONFIRM_RESTORE_TRASHED_POST_DIALOG_TAG" private const val CONFIRM_TRASH_POST_WITH_LOCAL_CHANGES_DIALOG_TAG = "CONFIRM_TRASH_POST_WITH_LOCAL_CHANGES_DIALOG_TAG" private const val CONFIRM_ON_CONFLICT_LOAD_REMOTE_POST_DIALOG_TAG = "CONFIRM_ON_CONFLICT_LOAD_REMOTE_POST_DIALOG_TAG" private const val CONFIRM_ON_AUTOSAVE_REVISION_DIALOG_TAG = "CONFIRM_ON_AUTOSAVE_REVISION_DIALOG_TAG" @@ -29,12 +29,24 @@ class PostListDialogHelper( ) { // Since we are using DialogFragments we need to hold onto which post will be published or trashed / resolved private var localPostIdForDeleteDialog: Int? = null - private var localPostIdForPublishDialog: Int? = null + private var localPostIdForMoveTrashedPostToDraftDialog: Int? = null private var localPostIdForTrashPostWithLocalChangesDialog: Int? = null private var localPostIdForConflictResolutionDialog: Int? = null private var localPostIdForAutosaveRevisionResolutionDialog: Int? = null private var localPostIdForScheduledPostSyncDialog: Int? = null + fun showMoveTrashedPostToDraftDialog(post: PostModel) { + val dialogHolder = DialogHolder( + tag = CONFIRM_RESTORE_TRASHED_POST_DIALOG_TAG, + title = UiStringRes(R.string.post_list_move_trashed_post_to_draft_dialog_title), + message = UiStringRes(R.string.post_list_move_trashed_post_to_draft_dialog_message), + positiveButton = UiStringRes(R.string.post_list_move_trashed_post_to_draft_dialog_positive), + negativeButton = UiStringRes(R.string.post_list_move_trashed_post_to_draft_dialog_negative) + ) + localPostIdForMoveTrashedPostToDraftDialog = post.id + showDialog.invoke(dialogHolder) + } + fun showDeletePostConfirmationDialog(post: PostModel) { // We need network connection to delete a remote post, but not a local draft if (!post.isLocalDraft && !checkNetworkConnection.invoke()) { @@ -51,22 +63,6 @@ class PostListDialogHelper( showDialog.invoke(dialogHolder) } - fun showPublishConfirmationDialog(post: PostModel) { - if (localPostIdForPublishDialog != null) { - // We can only handle one publish dialog at once - return - } - val dialogHolder = DialogHolder( - tag = CONFIRM_PUBLISH_POST_DIALOG_TAG, - title = UiStringRes(R.string.dialog_confirm_publish_title), - message = UiStringRes(R.string.dialog_confirm_publish_message_post), - positiveButton = UiStringRes(R.string.dialog_confirm_publish_yes), - negativeButton = UiStringRes(R.string.cancel) - ) - localPostIdForPublishDialog = post.id - showDialog.invoke(dialogHolder) - } - fun showSyncScheduledPostConfirmationDialog(post: PostModel) { if (localPostIdForScheduledPostSyncDialog != null) { // We can only handle one sync post dialog at once @@ -129,17 +125,14 @@ class PostListDialogHelper( deletePost: (Int) -> Unit, publishPost: (Int) -> Unit, updateConflictedPostWithRemoteVersion: (Int) -> Unit, - editRestoredAutoSavePost: (Int) -> Unit + editRestoredAutoSavePost: (Int) -> Unit, + moveTrashedPostToDraft: (Int) -> Unit ) { when (instanceTag) { CONFIRM_DELETE_POST_DIALOG_TAG -> localPostIdForDeleteDialog?.let { localPostIdForDeleteDialog = null deletePost(it) } - CONFIRM_PUBLISH_POST_DIALOG_TAG -> localPostIdForPublishDialog?.let { - localPostIdForPublishDialog = null - publishPost(it) - } CONFIRM_SYNC_SCHEDULED_POST_DIALOG_TAG -> localPostIdForScheduledPostSyncDialog?.let { localPostIdForScheduledPostSyncDialog = null publishPost(it) @@ -153,6 +146,10 @@ class PostListDialogHelper( localPostIdForTrashPostWithLocalChangesDialog = null trashPostWithLocalChanges(it) } + CONFIRM_RESTORE_TRASHED_POST_DIALOG_TAG -> localPostIdForMoveTrashedPostToDraftDialog?.let { + localPostIdForMoveTrashedPostToDraftDialog = null + moveTrashedPostToDraft(it) + } CONFIRM_ON_AUTOSAVE_REVISION_DIALOG_TAG -> localPostIdForAutosaveRevisionResolutionDialog?.let { // open the editor with the restored auto save localPostIdForAutosaveRevisionResolutionDialog = null @@ -172,7 +169,6 @@ class PostListDialogHelper( ) { when (instanceTag) { CONFIRM_DELETE_POST_DIALOG_TAG -> localPostIdForDeleteDialog = null - CONFIRM_PUBLISH_POST_DIALOG_TAG -> localPostIdForPublishDialog = null CONFIRM_SYNC_SCHEDULED_POST_DIALOG_TAG -> localPostIdForScheduledPostSyncDialog = null CONFIRM_TRASH_POST_WITH_LOCAL_CHANGES_DIALOG_TAG -> localPostIdForTrashPostWithLocalChangesDialog = null CONFIRM_ON_CONFLICT_LOAD_REMOTE_POST_DIALOG_TAG -> localPostIdForConflictResolutionDialog?.let { @@ -186,6 +182,7 @@ class PostListDialogHelper( mapOf(POST_TYPE to "post") ) } + CONFIRM_RESTORE_TRASHED_POST_DIALOG_TAG -> localPostIdForMoveTrashedPostToDraftDialog = null else -> throw IllegalArgumentException("Dialog's negative button click is not handled: $instanceTag") } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListItemViewHolder.kt index 71c8d4e0ad95..781c16e13bee 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListItemViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListItemViewHolder.kt @@ -11,6 +11,7 @@ import android.widget.ImageView import android.widget.ImageView.ScaleType import android.widget.PopupMenu import android.widget.ProgressBar +import android.widget.TextView import androidx.annotation.ColorRes import androidx.annotation.LayoutRes import androidx.constraintlayout.widget.ConstraintLayout @@ -42,7 +43,7 @@ sealed class PostListItemViewHolder( ) : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(layout, parent, false)) { private val featuredImageView: ImageView = itemView.findViewById(R.id.image_featured) private val titleTextView: WPTextView = itemView.findViewById(R.id.title) - private val dateAndAuthorTextView: WPTextView = itemView.findViewById(R.id.date_and_author) + private val postInfoTextView: WPTextView = itemView.findViewById(R.id.post_info) private val statusesTextView: WPTextView = itemView.findViewById(R.id.statuses_label) private val uploadProgressBar: ProgressBar = itemView.findViewById(R.id.upload_progress) private val disabledOverlay: FrameLayout = itemView.findViewById(R.id.disabled_overlay) @@ -119,7 +120,7 @@ sealed class PostListItemViewHolder( protected fun setBasicValues(data: PostListItemUiStateData) { uiHelpers.setTextOrHide(titleTextView, data.title) - uiHelpers.setTextOrHide(dateAndAuthorTextView, data.dateAndAuthor) + updatePostInfoLabel(postInfoTextView, data.postInfo) uiHelpers.updateVisibility(statusesTextView, data.statuses.isNotEmpty()) updateStatusesLabel(statusesTextView, data.statuses, data.statusesDelimiter, data.statusesColor) showFeaturedImage(data.imageUrl) @@ -132,6 +133,13 @@ sealed class PostListItemViewHolder( } } + private fun updatePostInfoLabel(view: TextView, uiStrings: List?) { + val concatenatedText = uiStrings?.joinToString(separator = " · ") { + uiHelpers.getTextOfUiString(view.context, it) + } + uiHelpers.setTextOrHide(view, concatenatedText) + } + protected fun onMoreClicked(actions: List, v: View) { val menu = PopupMenu(v.context, v) actions.forEach { singleItemAction -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt index ffd95cb75b3b..6c631d518810 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt @@ -20,6 +20,7 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat.POST_LIST_TAB_CHANG import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.generated.ListActionBuilder import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId +import org.wordpress.android.fluxc.model.PostModel import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.list.PostListDescriptor import org.wordpress.android.fluxc.model.post.PostStatus @@ -47,6 +48,7 @@ import org.wordpress.android.util.SiteUtils import org.wordpress.android.util.ToastUtils.Duration import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.analytics.AnalyticsUtils +import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.SingleLiveEvent import org.wordpress.android.viewmodel.helpers.DialogHolder import org.wordpress.android.viewmodel.helpers.ToastMessageHolder @@ -88,6 +90,8 @@ class PostListMainViewModel @Inject constructor( get() = bgDispatcher + scrollToTargetPostJob private lateinit var site: SiteModel + private lateinit var editPostRepository: EditPostRepository + var currentBottomSheetPostId: LocalId? = null private val _viewState = MutableLiveData() val viewState: LiveData = _viewState @@ -104,6 +108,9 @@ class PostListMainViewModel @Inject constructor( private val _scrollToLocalPostId = SingleLiveEvent() val scrollToLocalPostId = _scrollToLocalPostId as LiveData + private val _openPrepublishingBottomSheet = MutableLiveData>() + val openPrepublishingBottomSheet: LiveData> = _openPrepublishingBottomSheet + private val _snackBarMessage = SingleLiveEvent() val snackBarMessage = _snackBarMessage as LiveData @@ -174,6 +181,7 @@ class PostListMainViewModel @Inject constructor( hasUnhandledAutoSave = postConflictResolver::hasUnhandledAutoSave, triggerPostListAction = { _postListAction.postValue(it) }, triggerPostUploadAction = { _postUploadAction.postValue(it) }, + triggerPublishAction = this::showPrepublishingBottomSheet, invalidateList = this::invalidateAllLists, checkNetworkConnection = this::checkNetworkConnection, showSnackbar = { _snackBarMessage.postValue(it) }, @@ -199,8 +207,14 @@ class PostListMainViewModel @Inject constructor( lifecycleRegistry.markState(Lifecycle.State.CREATED) } - fun start(site: SiteModel, initPreviewState: PostListRemotePreviewState) { + fun start( + site: SiteModel, + initPreviewState: PostListRemotePreviewState, + currentBottomSheetPostId: LocalId, + editPostRepository: EditPostRepository + ) { this.site = site + this.editPostRepository = editPostRepository if (isSearchExpanded.value == true) { setViewLayoutAndIcon(COMPACT, false) @@ -250,6 +264,12 @@ class PostListMainViewModel @Inject constructor( ) _previewState.value = _previewState.value ?: initPreviewState + currentBottomSheetPostId.let { postId -> + if (postId.value != 0) { + editPostRepository.loadPostByLocalPostId(postId.value) + } + } + lifecycleRegistry.markState(Lifecycle.State.STARTED) uploadStarter.queueUploadFromSite(site) @@ -391,7 +411,8 @@ class PostListMainViewModel @Inject constructor( deletePost = postActionHandler::deletePost, publishPost = postActionHandler::publishPost, updateConflictedPostWithRemoteVersion = postConflictResolver::updateConflictedPostWithRemoteVersion, - editRestoredAutoSavePost = this::editRestoredAutoSavePost + editRestoredAutoSavePost = this::editRestoredAutoSavePost, + moveTrashedPostToDraft = postActionHandler::moveTrashedPostToDraft ) } @@ -411,6 +432,12 @@ class PostListMainViewModel @Inject constructor( ) } + private fun showPrepublishingBottomSheet(post: PostModel) { + currentBottomSheetPostId = LocalId(post.id) + editPostRepository.loadPostByLocalPostId(post.id) + _openPrepublishingBottomSheet.postValue(Event(Unit)) + } + /** * Only the non-null variables will be changed in the current state */ @@ -517,4 +544,10 @@ class PostListMainViewModel @Inject constructor( val savedLayoutType = prefs.postListViewLayoutType setViewLayoutAndIcon(savedLayoutType, false) } + + fun onBottomSheetPublishButtonClicked() { + editPostRepository.getEditablePost()?.let { + postActionHandler.publishPost(it) + } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostNotificationScheduleTimeDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostNotificationScheduleTimeDialogFragment.kt index 850d882081f0..24c68469adb8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostNotificationScheduleTimeDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostNotificationScheduleTimeDialogFragment.kt @@ -15,15 +15,27 @@ import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.Schedul import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.ONE_HOUR import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.TEN_MINUTES import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.WHEN_PUBLISHED +import org.wordpress.android.ui.posts.PublishSettingsFragmentType.EDIT_POST +import org.wordpress.android.ui.posts.PublishSettingsFragmentType.PREPUBLISHING_NUDGES +import org.wordpress.android.ui.posts.prepublishing.PrepublishingPublishSettingsViewModel import javax.inject.Inject class PostNotificationScheduleTimeDialogFragment : DialogFragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - private lateinit var viewModel: EditPostPublishSettingsViewModel + private lateinit var viewModel: PublishSettingsViewModel override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) - .get(EditPostPublishSettingsViewModel::class.java) + val publishSettingsFragmentType = arguments?.getParcelable( + ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE + ) + + when (publishSettingsFragmentType) { + EDIT_POST -> viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) + .get(EditPostPublishSettingsViewModel::class.java) + PREPUBLISHING_NUDGES -> viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) + .get(PrepublishingPublishSettingsViewModel::class.java) + } + val alertDialogBuilder = MaterialAlertDialogBuilder(activity) val view = requireActivity().layoutInflater.inflate(R.layout.post_notification_type_selector, null) as RadioGroup @@ -70,14 +82,22 @@ class PostNotificationScheduleTimeDialogFragment : DialogFragment() { companion object { const val TAG = "post_notification_time_dialog_fragment" const val ARG_NOTIFICATION_SCHEDULE_TIME = "notification_schedule_time" + private const val ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE = "publish_settings_fragment_type" - fun newInstance(period: SchedulingReminderModel.Period?): PostNotificationScheduleTimeDialogFragment { + fun newInstance( + period: SchedulingReminderModel.Period?, + publishSettingsFragmentType: PublishSettingsFragmentType + ): PostNotificationScheduleTimeDialogFragment { val fragment = PostNotificationScheduleTimeDialogFragment() + val args = Bundle() + args.putParcelable( + ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE, + publishSettingsFragmentType + ) period?.let { - val args = Bundle() args.putString(ARG_NOTIFICATION_SCHEDULE_TIME, period.name) - fragment.arguments = args } + fragment.arguments = args return fragment } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsInputDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsInputDialogFragment.java index c9573849a905..407083625c3a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsInputDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsInputDialogFragment.java @@ -21,7 +21,9 @@ import org.wordpress.android.util.ActivityUtils; public class PostSettingsInputDialogFragment extends DialogFragment implements TextWatcher { - interface PostSettingsInputDialogListener { + public static final String TAG = "post_settings_input_dialog_fragment"; + + public interface PostSettingsInputDialogListener { void onInputUpdated(String input); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsActivity.java index 5a0faaeceed0..db3e82ff28a8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsActivity.java @@ -1,67 +1,33 @@ package org.wordpress.android.ui.posts; -import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.RelativeLayout; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; +import androidx.fragment.app.Fragment; -import org.apache.commons.text.StringEscapeUtils; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; import org.wordpress.android.R; import org.wordpress.android.WordPress; -import org.wordpress.android.fluxc.Dispatcher; import org.wordpress.android.fluxc.model.SiteModel; -import org.wordpress.android.fluxc.model.TermModel; -import org.wordpress.android.fluxc.store.TaxonomyStore; -import org.wordpress.android.fluxc.store.TaxonomyStore.OnTaxonomyChanged; import org.wordpress.android.ui.LocaleAwareActivity; -import org.wordpress.android.util.ActivityUtils; import org.wordpress.android.util.ToastUtils; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import javax.inject.Inject; - -public class PostSettingsTagsActivity extends LocaleAwareActivity implements TextWatcher, View.OnKeyListener { +public class PostSettingsTagsActivity extends LocaleAwareActivity implements TagsSelectedListener { public static final String KEY_TAGS = "KEY_TAGS"; public static final String KEY_SELECTED_TAGS = "KEY_SELECTED_TAGS"; private SiteModel mSite; - - private EditText mTagsEditText; - private TagsRecyclerViewAdapter mAdapter; - - @Inject Dispatcher mDispatcher; - @Inject TaxonomyStore mTaxonomyStore; + private String mTags; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ((WordPress) getApplicationContext()).component().inject(this); - String tags = null; if (savedInstanceState == null) { mSite = (SiteModel) getIntent().getSerializableExtra(WordPress.SITE); - tags = getIntent().getStringExtra(KEY_TAGS); + mTags = getIntent().getStringExtra(KEY_TAGS); } else { mSite = (SiteModel) savedInstanceState.getSerializable(WordPress.SITE); } @@ -81,36 +47,17 @@ public void onCreate(Bundle savedInstanceState) { actionBar.setDisplayHomeAsUpEnabled(true); } - RecyclerView recyclerView = (RecyclerView) findViewById(R.id.tags_suggestion_list); - recyclerView.setHasFixedSize(true); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - - mAdapter = new TagsRecyclerViewAdapter(this); - mAdapter.setAllTags(mTaxonomyStore.getTagsForSite(mSite)); - recyclerView.setAdapter(mAdapter); - - mTagsEditText = (EditText) findViewById(R.id.tags_edit_text); - mTagsEditText.setOnKeyListener(this); - mTagsEditText.addTextChangedListener(this); - if (!TextUtils.isEmpty(tags)) { - // add a , at the end so the user can start typing a new tag - tags += ","; - tags = StringEscapeUtils.unescapeHtml4(tags); - mTagsEditText.setText(tags); - mTagsEditText.setSelection(mTagsEditText.length()); - } + postponeEnterTransition(); + showPostSettingsTagsFragment(); } - @Override - public void onStart() { - super.onStart(); - mDispatcher.register(this); - } + private void showPostSettingsTagsFragment() { + PostSettingsTagsFragment postSettingsTagsFragment = PostSettingsTagsFragment.newInstance(mSite, mTags); - @Override - public void onStop() { - mDispatcher.unregister(this); - super.onStop(); + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.fragment_container, postSettingsTagsFragment, PostSettingsTagsFragment.TAG) + .commit(); } @Override @@ -137,163 +84,24 @@ public void onBackPressed() { } private void saveAndFinish() { - ActivityUtils.hideKeyboardForced(mTagsEditText); + closeKeyboard(); Bundle bundle = new Bundle(); - bundle.putString(KEY_SELECTED_TAGS, mTagsEditText.getText().toString()); + bundle.putString(KEY_SELECTED_TAGS, mTags); Intent intent = new Intent(); intent.putExtras(bundle); setResult(RESULT_OK, intent); finish(); } - @Override - public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { - if ((keyEvent.getAction() == KeyEvent.ACTION_DOWN) - && (keyCode == KeyEvent.KEYCODE_ENTER)) { - // Since we don't allow new lines, we should add comma on "enter" to separate the tags - String currentText = mTagsEditText.getText().toString(); - if (!currentText.isEmpty() && !currentText.endsWith(",")) { - mTagsEditText.setText(currentText.concat(",")); - mTagsEditText.setSelection(mTagsEditText.length()); - } - return true; - } - return false; - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { - // No-op - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - filterListForCurrentText(); - } - - @Override - public void afterTextChanged(Editable editable) { - // No-op - } - - // Find the text after the last occurrence of "," and filter with it - private void filterListForCurrentText() { - String text = mTagsEditText.getText().toString(); - int endIndex = text.lastIndexOf(","); - if (endIndex == -1) { - mAdapter.filter(text); - } else { - String textToFilter = text.substring(endIndex + 1).trim(); - mAdapter.filter(textToFilter); - } - } - - private void onTagSelected(@NonNull String selectedTag) { - String text = mTagsEditText.getText().toString(); - String updatedText; - int endIndex = text.lastIndexOf(","); - if (endIndex == -1) { - // no "," found, replace the current text with the selectedTag - updatedText = selectedTag; - } else { - // there are multiple tags already, only update the text after the last "," - updatedText = text.substring(0, endIndex + 1) + selectedTag; - } - updatedText += ","; - updatedText = StringEscapeUtils.unescapeHtml4(updatedText); - mTagsEditText.setText(updatedText); - mTagsEditText.setSelection(mTagsEditText.length()); - } - - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onTaxonomyChanged(OnTaxonomyChanged event) { - switch (event.causeOfChange) { - case FETCH_TAGS: - mAdapter.setAllTags(mTaxonomyStore.getTagsForSite(mSite)); - filterListForCurrentText(); - break; + private void closeKeyboard() { + Fragment fragment = getSupportFragmentManager().findFragmentByTag(PostSettingsTagsFragment.TAG); + if (fragment != null) { + ((PostSettingsTagsFragment) fragment).closeKeyboard(); } } - private class TagsRecyclerViewAdapter extends RecyclerView.Adapter { - private List mAllTags; - private List mFilteredTags; - private Context mContext; - - TagsRecyclerViewAdapter(Context context) { - mContext = context; - mFilteredTags = new ArrayList<>(); - } - - @Override - public TagViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.tags_list_row, parent, false); - return new TagViewHolder(view); - } - - @Override - public void onBindViewHolder(final TagViewHolder holder, int position) { - // Guard against mFilteredTags getting altered in another thread - if (mFilteredTags.size() <= position) { - return; - } - String tag = StringEscapeUtils.unescapeHtml4(mFilteredTags.get(position).getName()); - holder.mNameTextView.setText(tag); - } - - @Override - public int getItemCount() { - return mFilteredTags.size(); - } - - void setAllTags(List allTags) { - mAllTags = allTags; - } - - public void filter(final String text) { - final List allTags = mAllTags; - new Thread(new Runnable() { - @Override - public void run() { - final List filteredTags = new ArrayList<>(); - if (TextUtils.isEmpty(text)) { - filteredTags.addAll(allTags); - } else { - for (TermModel tag : allTags) { - if (tag.getName().toLowerCase(Locale.getDefault()) - .contains(text.toLowerCase(Locale.getDefault()))) { - filteredTags.add(tag); - } - } - } - - ((Activity) mContext).runOnUiThread(new Runnable() { - @Override - public void run() { - mFilteredTags = filteredTags; - notifyDataSetChanged(); - } - }); - } - }).start(); - } - - class TagViewHolder extends RecyclerView.ViewHolder { - private final TextView mNameTextView; - - TagViewHolder(View view) { - super(view); - mNameTextView = (TextView) view.findViewById(R.id.tag_name); - RelativeLayout layout = (RelativeLayout) view.findViewById(R.id.tags_list_row_container); - layout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - onTagSelected(mNameTextView.getText().toString()); - } - }); - } - } + @Override public void onTagsSelected(@NonNull String selectedTags) { + mTags = selectedTags; } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsFragment.kt new file mode 100644 index 000000000000..c5e10c639063 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsFragment.kt @@ -0,0 +1,43 @@ +package org.wordpress.android.ui.posts + +import android.content.Context +import android.os.Bundle +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.fluxc.model.SiteModel + +class PostSettingsTagsFragment : TagsFragment() { + override fun getContentLayout() = R.layout.fragment_post_settings_tags + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + mTagsSelectedListener = if (context is PostSettingsTagsActivity) { + context + } else { + throw RuntimeException("$context must implement TagsSelectedListener") + } + } + + override fun onDetach() { + super.onDetach() + mTagsSelectedListener = null + } + + override fun getTagsFromEditPostRepositoryOrArguments() = arguments?.getString(PostSettingsTagsActivity.KEY_TAGS) + + companion object { + const val TAG = "post_settings_tags_fragment_tag" + @JvmStatic fun newInstance(site: SiteModel, tags: String?): PostSettingsTagsFragment { + val bundle = Bundle().apply { + putSerializable(WordPress.SITE, site) + putString(PostSettingsTagsActivity.KEY_TAGS, tags) + } + return PostSettingsTagsFragment().apply { arguments = bundle } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsUtils.kt index 9382a1616648..b7eb2e862829 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsUtils.kt @@ -11,7 +11,8 @@ import javax.inject.Inject class PostSettingsUtils @Inject constructor( private val resourceProvider: ResourceProvider, - private val dateUtils: DateUtils + private val dateUtils: DateUtils, + private val postUtilsWrapper: PostUtilsWrapper ) { fun getPublishDateLabel( postModel: PostImmutableModel @@ -22,28 +23,29 @@ class PostSettingsUtils if (!TextUtils.isEmpty(dateCreated)) { val formattedDate = dateUtils.formatDateTime(dateCreated) - if (status == PostStatus.SCHEDULED) { - labelToUse = resourceProvider.getString(R.string.scheduled_for, formattedDate) - } else if (status == PostStatus.PUBLISHED || status == PostStatus.PRIVATE) { - labelToUse = resourceProvider.getString(R.string.published_on, formattedDate) - } else if (postModel.isLocalDraft) { - if (PostUtils.isPublishDateInThePast(postModel.dateCreated)) { + if (postModel.isLocalDraft) { + if (postUtilsWrapper.isPublishDateInThePast(postModel.dateCreated)) { labelToUse = resourceProvider.getString(R.string.backdated_for, formattedDate) - } else if (PostUtils.shouldPublishImmediately(status, postModel.dateCreated)) { + } else if (postUtilsWrapper.isPublishDateInTheFuture(postModel.dateCreated)) { + labelToUse = resourceProvider.getString(R.string.schedule_for, formattedDate) + } else if (postUtilsWrapper.shouldPublishImmediately(status, postModel.dateCreated)) { labelToUse = resourceProvider.getString(R.string.immediately) } else { labelToUse = resourceProvider.getString(R.string.publish_on, formattedDate) } - } else if (PostUtils.isPublishDateInTheFuture(postModel.dateCreated)) { + } else if (status == PostStatus.SCHEDULED) { + labelToUse = resourceProvider.getString(R.string.scheduled_for, formattedDate) + } else if (status == PostStatus.PUBLISHED || status == PostStatus.PRIVATE) { + labelToUse = resourceProvider.getString(R.string.published_on, formattedDate) + } else if (postUtilsWrapper.isPublishDateInTheFuture(postModel.dateCreated)) { labelToUse = resourceProvider.getString(R.string.schedule_for, formattedDate) } else { labelToUse = resourceProvider.getString(R.string.publish_on, formattedDate) } - } else if (PostUtils.shouldPublishImmediatelyOptionBeAvailable(status)) { + } else if (postUtilsWrapper.shouldPublishImmediatelyOptionBeAvailable(status)) { labelToUse = resourceProvider.getString(R.string.immediately) } else { - // TODO: What should the label be if there is no specific date and this is not a DRAFT? - labelToUse = "" + labelToUse = resourceProvider.getString(R.string.immediately) } return labelToUse } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostTimePickerDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostTimePickerDialogFragment.kt index 53c9c0bad9a5..6ec808e963c3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostTimePickerDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostTimePickerDialogFragment.kt @@ -10,18 +10,31 @@ import androidx.fragment.app.DialogFragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import org.wordpress.android.WordPress +import org.wordpress.android.ui.posts.PublishSettingsFragmentType.EDIT_POST +import org.wordpress.android.ui.posts.PublishSettingsFragmentType.PREPUBLISHING_NUDGES + +import org.wordpress.android.ui.posts.prepublishing.PrepublishingPublishSettingsViewModel import javax.inject.Inject class PostTimePickerDialogFragment : DialogFragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - private lateinit var viewModel: EditPostPublishSettingsViewModel + private lateinit var viewModel: PublishSettingsViewModel override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) - .get(EditPostPublishSettingsViewModel::class.java) + val publishSettingsFragmentType = arguments?.getParcelable( + ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE + ) + + when (publishSettingsFragmentType) { + EDIT_POST -> viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) + .get(EditPostPublishSettingsViewModel::class.java) + PREPUBLISHING_NUDGES -> viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) + .get(PrepublishingPublishSettingsViewModel::class.java) + } val is24HrFormat = DateFormat.is24HourFormat(activity) - val timePickerDialog = TimePickerDialog(activity, + val timePickerDialog = TimePickerDialog( + activity, OnTimeSetListener { _, selectedHour, selectedMinute -> viewModel.onTimeSelected(selectedHour, selectedMinute) }, @@ -39,9 +52,17 @@ class PostTimePickerDialogFragment : DialogFragment() { companion object { const val TAG = "post_time_picker_dialog_fragment" + const val ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE = "publish_settings_fragment_type" - fun newInstance(): PostTimePickerDialogFragment { - return PostTimePickerDialogFragment() + fun newInstance(publishSettingsFragmentType: PublishSettingsFragmentType): PostTimePickerDialogFragment { + return PostTimePickerDialogFragment().apply { + arguments = Bundle().apply { + putParcelable( + ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE, + publishSettingsFragmentType + ) + } + } } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java index 1ce4cf240a9c..7d5162518fbb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java @@ -328,17 +328,21 @@ static boolean shouldPublishImmediately(PostStatus postStatus, String dateCreate return pubDate == null || !pubDate.after(now); } - static boolean isPublishDateInTheFuture(String dateCreated) { + public static boolean isPublishDateInTheFuture(String dateCreated) { + return isPublishDateInTheFuture(dateCreated, new Date()); + } + + public static boolean isPublishDateInTheFuture(String dateCreated, Date currentDate) { Date pubDate = DateTimeUtils.dateFromIso8601(dateCreated); - Date now = new Date(); - return pubDate != null && pubDate.after(now); + return pubDate != null && pubDate.after(currentDate); } - static boolean isPublishDateInThePast(String dateCreated) { + public static boolean isPublishDateInThePast(String dateCreated, Date currentDate) { Date pubDate = DateTimeUtils.dateFromIso8601(dateCreated); - // just use half an hour before now as a threshold to make sure this is backdated, to avoid false positives Calendar cal = Calendar.getInstance(); + cal.setTime(currentDate); + // just use half an hour before now as a threshold to make sure this is backdated, to avoid false positives cal.add(Calendar.MINUTE, -30); Date halfHourBack = cal.getTime(); return pubDate != null && pubDate.before(halfHourBack); @@ -350,7 +354,7 @@ static boolean shouldPublishImmediatelyOptionBeAvailable(PostModel postModel) { } static boolean shouldPublishImmediatelyOptionBeAvailable(PostStatus postStatus) { - return postStatus == PostStatus.DRAFT; + return postStatus == PostStatus.DRAFT || postStatus == PostStatus.PRIVATE; } static boolean shouldPublishImmediatelyOptionBeAvailable(String postStatus) { 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 17792c7c9ad6..5ebf27043692 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 @@ -4,6 +4,7 @@ import dagger.Reusable import org.wordpress.android.fluxc.model.PostImmutableModel import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.post.PostStatus +import org.wordpress.android.ui.reader.utils.DateProvider import javax.inject.Inject /** @@ -14,7 +15,7 @@ import javax.inject.Inject * */ @Reusable -class PostUtilsWrapper @Inject constructor() { +class PostUtilsWrapper @Inject constructor(private val dateProvider: DateProvider) { fun isPublishable(post: PostImmutableModel) = PostUtils.isPublishable(post) fun isPostInConflictWithRemote(post: PostImmutableModel) = @@ -37,4 +38,13 @@ class PostUtilsWrapper @Inject constructor() { fun trackSavePostAnalytics(post: PostImmutableModel?, site: SiteModel) = PostUtils.trackSavePostAnalytics(post, site) + + fun isPublishDateInTheFuture(dateCreated: String) = + PostUtils.isPublishDateInTheFuture(dateCreated, dateProvider.getCurrentDate()) + + fun isPublishDateInThePast(dateCreated: String) = + PostUtils.isPublishDateInThePast(dateCreated, dateProvider.getCurrentDate()) + + fun shouldPublishImmediatelyOptionBeAvailable(status: PostStatus?) = + PostUtils.shouldPublishImmediatelyOptionBeAvailable(status) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt index ebfab3643823..9446391fd1b4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt @@ -34,6 +34,7 @@ import org.greenrobot.eventbus.ThreadMode import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.QuickStartStore import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask @@ -46,8 +47,11 @@ import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogNegativeClickInterface import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogOnDismissByOutsideTouchInterface import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogPositiveClickInterface +import org.wordpress.android.ui.posts.EditPostSettingsFragment.EditPostActivityHook import org.wordpress.android.ui.posts.PostListType.SEARCH +import org.wordpress.android.ui.posts.PrepublishingBottomSheetFragment.Companion.newInstance import org.wordpress.android.ui.posts.adapters.AuthorSelectionAdapter +import org.wordpress.android.ui.posts.prepublishing.PrepublishingBottomSheetListener import org.wordpress.android.ui.quickstart.QuickStartEvent import org.wordpress.android.ui.uploads.UploadActionUseCase import org.wordpress.android.ui.uploads.UploadUtilsWrapper @@ -66,8 +70,11 @@ import javax.inject.Inject const val EXTRA_TARGET_POST_LOCAL_ID = "targetPostLocalId" const val STATE_KEY_PREVIEW_STATE = "stateKeyPreviewState" +const val STATE_KEY_BOTTOMSHEET_POST_ID = "stateKeyBottomSheetPostId" class PostsListActivity : LocaleAwareActivity(), + EditPostActivityHook, + PrepublishingBottomSheetListener, BasicDialogPositiveClickInterface, BasicDialogNegativeClickInterface, BasicDialogOnDismissByOutsideTouchInterface { @@ -82,8 +89,13 @@ class PostsListActivity : LocaleAwareActivity(), @Inject internal lateinit var snackbarSequencer: SnackbarSequencer @Inject internal lateinit var uploadUtilsWrapper: UploadUtilsWrapper @Inject internal lateinit var quickStartStore: QuickStartStore + @Inject internal lateinit var editPostRepository: EditPostRepository private lateinit var site: SiteModel + + override fun getSite() = site + override fun getEditPostRepository() = editPostRepository + private lateinit var viewModel: PostListMainViewModel private lateinit var authorSelectionAdapter: AuthorSelectionAdapter @@ -152,9 +164,15 @@ class PostsListActivity : LocaleAwareActivity(), PostListRemotePreviewState.fromInt(savedInstanceState.getInt(STATE_KEY_PREVIEW_STATE, 0)) } + val currentBottomSheetPostId = if (savedInstanceState == null) { + LocalId(0) + } else { + LocalId(savedInstanceState.getInt(STATE_KEY_BOTTOMSHEET_POST_ID, 0)) + } + setupActionBar() setupContent() - initViewModel(initPreviewState) + initViewModel(initPreviewState, currentBottomSheetPostId) loadIntentData(intent) quickStartEvent = savedInstanceState?.getParcelable(QuickStartEvent.KEY) @@ -227,9 +245,9 @@ class PostsListActivity : LocaleAwareActivity(), pager.adapter = postsPagerAdapter } - private fun initViewModel(initPreviewState: PostListRemotePreviewState) { + private fun initViewModel(initPreviewState: PostListRemotePreviewState, currentBottomSheetPostId: LocalId) { viewModel = ViewModelProviders.of(this, viewModelFactory).get(PostListMainViewModel::class.java) - viewModel.start(site, initPreviewState) + viewModel.start(site, initPreviewState, currentBottomSheetPostId, editPostRepository) viewModel.viewState.observe(this, Observer { state -> state?.let { @@ -313,6 +331,15 @@ class PostsListActivity : LocaleAwareActivity(), ) } }) + viewModel.openPrepublishingBottomSheet.observe(this, Observer { event -> + event.applyIfNotHandled { + val fragment = supportFragmentManager.findFragmentByTag(PrepublishingBottomSheetFragment.TAG) + if (fragment == null) { + val prepublishingFragment = newInstance(site, editPostRepository.isPage) + prepublishingFragment.show(supportFragmentManager, PrepublishingBottomSheetFragment.TAG) + } + } + }) } private fun showSnackBar(holder: SnackbarMessageHolder) { @@ -484,6 +511,9 @@ class PostsListActivity : LocaleAwareActivity(), viewModel.previewState.value?.let { outState.putInt(STATE_KEY_PREVIEW_STATE, it.value) } + viewModel.currentBottomSheetPostId?.let { + outState.putInt(STATE_KEY_BOTTOMSHEET_POST_ID, it.value) + } } // BasicDialogFragment Callbacks @@ -549,4 +579,8 @@ class PostsListActivity : LocaleAwareActivity(), super.onStop() EventBus.getDefault().unregister(this) } + + override fun onSubmitButtonClicked(publishPost: PublishPost) { + viewModel.onBottomSheetPublishButtonClicked() + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingActionClickedListener.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingActionClickedListener.kt new file mode 100644 index 000000000000..e56e57bd40b8 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingActionClickedListener.kt @@ -0,0 +1,8 @@ +package org.wordpress.android.ui.posts + +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ActionType + +interface PrepublishingActionClickedListener { + fun onActionClicked(actionType: ActionType) + fun onSubmitButtonClicked(publishPost: PublishPost) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingBottomSheetFragment.kt new file mode 100644 index 000000000000..16b9719a1caa --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingBottomSheetFragment.kt @@ -0,0 +1,214 @@ +package org.wordpress.android.ui.posts + +import android.content.Context +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.annotation.NonNull +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import kotlinx.android.synthetic.main.post_prepublishing_bottom_sheet.* +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.login.widgets.WPBottomSheetDialogFragment +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ActionType +import org.wordpress.android.ui.posts.PrepublishingScreen.HOME +import org.wordpress.android.ui.posts.prepublishing.PrepublishingBottomSheetListener +import org.wordpress.android.ui.posts.prepublishing.PrepublishingPublishSettingsFragment +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityFragment +import javax.inject.Inject + +class PrepublishingBottomSheetFragment : WPBottomSheetDialogFragment(), + PrepublishingScreenClosedListener, PrepublishingActionClickedListener { + @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var viewModel: PrepublishingViewModel + + private var prepublishingBottomSheetListener: PrepublishingBottomSheetListener? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(DialogFragment.STYLE_NORMAL, R.style.WordPress_PrepublishingNudges_BottomSheetDialogTheme) + (requireNotNull(activity).application as WordPress).component().inject(this) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + prepublishingBottomSheetListener = if (context is PrepublishingBottomSheetListener) { + context + } else { + throw RuntimeException("$context must implement PrepublishingBottomSheetListener") + } + } + + override fun onDetach() { + super.onDetach() + prepublishingBottomSheetListener = null + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.post_prepublishing_bottom_sheet, container) + } + + override fun onResume() { + super.onResume() + /** + * The back button normally closes the bottom sheet so now instead of doing that it goes back to + * the home screen with the actions and only if pressed again will it close the bottom sheet. + */ + dialog?.setOnKeyListener { _, keyCode, event -> + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (event.action != KeyEvent.ACTION_DOWN) { + true + } else { + onBackClicked() + true + } + } else { + false + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initViewModel(savedInstanceState) + dialog?.setOnShowListener { dialogInterface -> + val sheetDialog = dialogInterface as? BottomSheetDialog + + val bottomSheet = sheetDialog?.findViewById( + com.google.android.material.R.id.design_bottom_sheet + ) as? FrameLayout + + bottomSheet?.let { + val behavior = BottomSheetBehavior.from(it) + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + setupMinimumHeightForFragmentContainer() + } + + private fun setupMinimumHeightForFragmentContainer() { + val isPage = checkNotNull(arguments?.getBoolean(IS_PAGE)) { + "arguments can't be null." + } + + if (isPage) { + prepublishing_content_fragment.minimumHeight = + resources.getDimensionPixelSize(R.dimen.prepublishing_fragment_container_min_height_for_page) + } else { + prepublishing_content_fragment.minimumHeight = + resources.getDimensionPixelSize(R.dimen.prepublishing_fragment_container_min_height) + } + } + + private fun initViewModel(savedInstanceState: Bundle?) { + viewModel = ViewModelProviders.of(this, viewModelFactory) + .get(PrepublishingViewModel::class.java) + + viewModel.navigationTarget.observe(this, Observer { event -> + event.getContentIfNotHandled()?.let { navigationState -> + navigateToScreen(navigationState) + } + }) + + viewModel.dismissBottomSheet.observe(this, Observer { event -> + event.applyIfNotHandled { dismiss() } + }) + + viewModel.triggerOnSubmitButtonClickedListener.observe(this, Observer { event -> + event.getContentIfNotHandled()?.let { publishPost -> + prepublishingBottomSheetListener?.onSubmitButtonClicked(publishPost) + } + }) + + val prepublishingScreenState = savedInstanceState?.getParcelable(KEY_SCREEN_STATE) + val site = arguments?.getSerializable(SITE) as SiteModel + + viewModel.start(site, prepublishingScreenState) + } + + private fun navigateToScreen(navigationTarget: PrepublishingNavigationTarget) { + val (fragment, tag) = when (navigationTarget.targetScreen) { + HOME -> Pair( + PrepublishingHomeFragment.newInstance(), + PrepublishingHomeFragment.TAG + ) + PrepublishingScreen.PUBLISH -> Pair( + PrepublishingPublishSettingsFragment.newInstance(), + PrepublishingPublishSettingsFragment.TAG + ) + PrepublishingScreen.VISIBILITY -> Pair( + PrepublishingVisibilityFragment.newInstance(), + PrepublishingVisibilityFragment.TAG + ) + PrepublishingScreen.TAGS -> Pair( + PrepublishingTagsFragment.newInstance(navigationTarget.site), PrepublishingTagsFragment.TAG + ) + } + + fadeInFragment(fragment, tag) + } + + private fun fadeInFragment(fragment: Fragment, tag: String) { + childFragmentManager.let { fragmentManager -> + val fragmentTransaction = fragmentManager.beginTransaction() + fragmentManager.findFragmentById(R.id.prepublishing_content_fragment)?.run { + fragmentTransaction.addToBackStack(null).setCustomAnimations( + R.anim.prepublishing_fragment_fade_in, R.anim.prepublishing_fragment_fade_out, + R.anim.prepublishing_fragment_fade_in, R.anim.prepublishing_fragment_fade_out + ) + } + fragmentTransaction.replace(R.id.prepublishing_content_fragment, fragment, tag) + fragmentTransaction.commit() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + viewModel.writeToBundle(outState) + } + + override fun onCloseClicked() { + viewModel.onCloseClicked() + } + + override fun onBackClicked() { + viewModel.onBackClicked() + } + + override fun onActionClicked(actionType: ActionType) { + viewModel.onActionClicked(actionType) + } + + override fun onSubmitButtonClicked(publishPost: PublishPost) { + viewModel.onSubmitButtonClicked(publishPost) + } + + companion object { + const val TAG = "prepublishing_bottom_sheet_fragment_tag" + const val SITE = "prepublishing_bottom_sheet_site_model" + const val IS_PAGE = "prepublishing_bottom_sheet_is_page" + + @JvmStatic + fun newInstance(@NonNull site: SiteModel, isPage: Boolean) = + PrepublishingBottomSheetFragment().apply { + arguments = Bundle().apply { + putSerializable(SITE, site) + putBoolean(IS_PAGE, isPage) + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeAdapter.kt new file mode 100644 index 000000000000..22cc7fb62820 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeAdapter.kt @@ -0,0 +1,65 @@ +package org.wordpress.android.ui.posts + +import android.content.Context +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import org.wordpress.android.WordPress +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ButtonUiState +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.HeaderUiState +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.HomeUiState +import org.wordpress.android.ui.posts.PrepublishingHomeViewHolder.PrepublishingHeaderListItemViewHolder +import org.wordpress.android.ui.posts.PrepublishingHomeViewHolder.PrepublishingHomeListItemViewHolder +import org.wordpress.android.ui.posts.PrepublishingHomeViewHolder.PrepublishingSubmitButtonViewHolder +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.image.ImageManager +import javax.inject.Inject + +private const val headerViewType: Int = 1 +private const val homeItemViewType: Int = 2 +private const val submitButtonViewType: Int = 3 + +class PrepublishingHomeAdapter(context: Context) : RecyclerView.Adapter() { + private var items: List = listOf() + @Inject lateinit var uiHelpers: UiHelpers + @Inject lateinit var imageManager: ImageManager + + init { + (context.applicationContext as WordPress).component().inject(this) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PrepublishingHomeViewHolder { + return when (viewType) { + headerViewType -> PrepublishingHeaderListItemViewHolder(parent, uiHelpers, imageManager) + homeItemViewType -> PrepublishingHomeListItemViewHolder(parent, uiHelpers) + submitButtonViewType -> PrepublishingSubmitButtonViewHolder(parent, uiHelpers) + else -> throw NotImplementedError("Unknown ViewType") + } + } + + fun update(newItems: List) { + val diffResult = DiffUtil.calculateDiff( + PrepublishingHomeDiffCallback( + this.items, + newItems + ) + ) + this.items = newItems + diffResult.dispatchUpdatesTo(this) + } + + override fun getItemViewType(position: Int): Int { + return when (items[position]) { + is HeaderUiState -> headerViewType + is HomeUiState -> homeItemViewType + is ButtonUiState -> submitButtonViewType + } + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: PrepublishingHomeViewHolder, position: Int) { + val item = items[position] + holder.onBind(item) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeDiffCallback.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeDiffCallback.kt new file mode 100644 index 000000000000..da30b38a50bb --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeDiffCallback.kt @@ -0,0 +1,24 @@ +package org.wordpress.android.ui.posts + +import androidx.recyclerview.widget.DiffUtil.Callback + +class PrepublishingHomeDiffCallback( + private val oldList: List, + private val newList: List +) : Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val newItem = newList[newItemPosition] + val oldItem = oldList[oldItemPosition] + + return (oldItem == newItem) + } + + override fun getOldListSize(): Int = oldList.size + + override fun getNewListSize(): Int = newList.size + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean = oldList[oldItemPosition] == newList[newItemPosition] +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeFragment.kt new file mode 100644 index 000000000000..7f9b3c8d2ad5 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeFragment.kt @@ -0,0 +1,112 @@ +package org.wordpress.android.ui.posts + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.android.synthetic.main.post_prepublishing_home_fragment.* +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.posts.EditPostSettingsFragment.EditPostActivityHook +import javax.inject.Inject + +class PrepublishingHomeFragment : Fragment() { + @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var viewModel: PrepublishingHomeViewModel + + private var actionClickedListener: PrepublishingActionClickedListener? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireNotNull(activity).application as WordPress).component().inject(this) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + actionClickedListener = parentFragment as PrepublishingActionClickedListener + } + + override fun onDetach() { + super.onDetach() + actionClickedListener = null + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.post_prepublishing_home_fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + actions_recycler_view.layoutManager = LinearLayoutManager(requireActivity()) + actions_recycler_view.adapter = PrepublishingHomeAdapter(requireActivity()) + + initViewModel() + } + + private fun initViewModel() { + viewModel = ViewModelProviders.of(this, viewModelFactory) + .get(PrepublishingHomeViewModel::class.java) + + viewModel.uiState.observe(viewLifecycleOwner, Observer { uiState -> + (actions_recycler_view.adapter as PrepublishingHomeAdapter).update(uiState) + }) + + viewModel.onActionClicked.observe(viewLifecycleOwner, Observer { event -> + event.getContentIfNotHandled()?.let { actionType -> + actionClickedListener?.onActionClicked(actionType) + } + }) + + viewModel.onSubmitButtonClicked.observe(viewLifecycleOwner, Observer { event -> + event.getContentIfNotHandled()?.let { publishPost -> + actionClickedListener?.onSubmitButtonClicked(publishPost) + } + }) + + viewModel.start(getEditPostRepository(), getSite()) + } + + private fun getSite(): SiteModel { + val editPostActivityHook = requireNotNull(getEditPostActivityHook()) { + "EditPostActivityHook shouldn't be null." + } + + return editPostActivityHook.site + } + + private fun getEditPostRepository(): EditPostRepository { + val editPostActivityHook = requireNotNull(getEditPostActivityHook()) { + "This is possibly null because it's " + + "called during config changes." + } + + return editPostActivityHook.editPostRepository + } + + private fun getEditPostActivityHook(): EditPostActivityHook? { + val activity = activity ?: return null + return if (activity is EditPostActivityHook) { + activity + } else { + throw RuntimeException("$activity must implement EditPostActivityHook") + } + } + + companion object { + const val TAG = "prepublishing_home_fragment_tag" + + fun newInstance() = PrepublishingHomeFragment() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeItemUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeItemUiState.kt new file mode 100644 index 000000000000..61ac403cd3c6 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeItemUiState.kt @@ -0,0 +1,61 @@ +package org.wordpress.android.ui.posts + +import androidx.annotation.ColorRes +import org.wordpress.android.R +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.ui.utils.UiString.UiStringText + +typealias PublishPost = Boolean + +sealed class PrepublishingHomeItemUiState { + data class HomeUiState( + val actionType: ActionType, + @ColorRes val actionTypeColor: Int = R.color.prepublishing_action_type_enabled_color, + var actionResult: UiString? = null, + @ColorRes val actionResultColor: Int = R.color.prepublishing_action_result_enabled_color, + val actionClickable: Boolean, + val onActionClicked: ((actionType: ActionType) -> Unit)? + ) : PrepublishingHomeItemUiState() + + data class HeaderUiState(val siteName: UiStringText, val siteIconUrl: String) : + PrepublishingHomeItemUiState() + + sealed class ButtonUiState( + val buttonText: UiStringRes, + val publishPost: PublishPost + ) : PrepublishingHomeItemUiState() { + open val onButtonClicked: ((PublishPost) -> Unit)? = null + + data class PublishButtonUiState(override val onButtonClicked: (PublishPost) -> Unit) : ButtonUiState( + UiStringRes(R.string.prepublishing_nudges_home_publish_button), + true + ) + + data class ScheduleButtonUiState(override val onButtonClicked: (PublishPost) -> Unit) : ButtonUiState( + UiStringRes(R.string.prepublishing_nudges_home_schedule_button), + false + ) + + data class UpdateButtonUiState(override val onButtonClicked: (PublishPost) -> Unit) : ButtonUiState( + UiStringRes(R.string.prepublishing_nudges_home_update_button), + false + ) + + data class SubmitButtonUiState(override val onButtonClicked: (PublishPost) -> Unit) : ButtonUiState( + UiStringRes(R.string.prepublishing_nudges_home_submit_button), + true + ) + + data class SaveButtonUiState(override val onButtonClicked: (PublishPost) -> Unit) : ButtonUiState( + UiStringRes(R.string.prepublishing_nudges_home_save_button), + false + ) + } + + enum class ActionType(val textRes: UiStringRes) { + PUBLISH(UiStringRes(R.string.prepublishing_nudges_publish_action)), + VISIBILITY(UiStringRes(R.string.prepublishing_nudges_visibility_action)), + TAGS(UiStringRes(R.string.prepublishing_nudges_tags_action)) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeViewHolder.kt new file mode 100644 index 000000000000..90e4b8db9e8b --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeViewHolder.kt @@ -0,0 +1,80 @@ +package org.wordpress.android.ui.posts + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.LayoutRes +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import org.wordpress.android.R +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ButtonUiState +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.HeaderUiState +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.HomeUiState +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.image.ImageManager +import org.wordpress.android.util.image.ImageType.BLAVATAR + +sealed class PrepublishingHomeViewHolder( + internal val parent: ViewGroup, + @LayoutRes layout: Int +) : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(layout, parent, false)) { + abstract fun onBind(uiState: PrepublishingHomeItemUiState) + + class PrepublishingHomeListItemViewHolder(parentView: ViewGroup, val uiHelpers: UiHelpers) : + PrepublishingHomeViewHolder(parentView, R.layout.prepublishing_action_list_item) { + private val actionType: TextView = itemView.findViewById(R.id.action_type) + private val actionResult: TextView = itemView.findViewById(R.id.action_result) + private val actionLayout: View = itemView.findViewById(R.id.action_layout) + + override fun onBind(uiState: PrepublishingHomeItemUiState) { + uiState as HomeUiState + + actionType.text = uiHelpers.getTextOfUiString(itemView.context, uiState.actionType.textRes) + uiState.actionResult?.let { resultText -> + actionResult.text = uiHelpers.getTextOfUiString(itemView.context, resultText) + } + + actionLayout.isEnabled = uiState.actionClickable + actionLayout.setOnClickListener { + uiState.onActionClicked?.invoke(uiState.actionType) + } + + actionType.setTextColor(ContextCompat.getColor(itemView.context, uiState.actionTypeColor)) + actionResult.setTextColor(ContextCompat.getColor(itemView.context, uiState.actionResultColor)) + } + } + + class PrepublishingHeaderListItemViewHolder( + parentView: ViewGroup, + val uiHelpers: UiHelpers, + val imageManager: ImageManager + ) : PrepublishingHomeViewHolder(parentView, R.layout.prepublishing_home_header_list_item) { + private val siteName: TextView = itemView.findViewById(R.id.site_name) + private val siteIcon: ImageView = itemView.findViewById(R.id.site_icon) + + override fun onBind(uiState: PrepublishingHomeItemUiState) { + uiState as HeaderUiState + + siteName.text = uiHelpers.getTextOfUiString(itemView.context, uiState.siteName) + + imageManager.load(siteIcon, BLAVATAR, uiState.siteIconUrl) + } + } + + class PrepublishingSubmitButtonViewHolder(parentView: ViewGroup, val uiHelpers: UiHelpers) : + PrepublishingHomeViewHolder(parentView, R.layout.prepublishing_home_publish_button_list_item) { + private val button: Button = itemView.findViewById(R.id.publish_button) + + override fun onBind(uiState: PrepublishingHomeItemUiState) { + uiState as ButtonUiState + + button.text = uiHelpers.getTextOfUiString(itemView.context, uiState.buttonText) + button.setOnClickListener { + uiState.onButtonClicked?.invoke(uiState.publishPost) + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModel.kt new file mode 100644 index 000000000000..a25019fbf910 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModel.kt @@ -0,0 +1,109 @@ +package org.wordpress.android.ui.posts + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.post.PostStatus +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ActionType +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ActionType.PUBLISH +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ActionType.TAGS +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ActionType.VISIBILITY +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.HeaderUiState +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.HomeUiState +import org.wordpress.android.ui.posts.prepublishing.home.usecases.GetButtonUiStateUseCase +import org.wordpress.android.ui.posts.prepublishing.visibility.usecases.GetPostVisibilityUseCase +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.util.StringUtils +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.viewmodel.Event +import javax.inject.Inject + +class PrepublishingHomeViewModel @Inject constructor( + private val getPostTagsUseCase: GetPostTagsUseCase, + private val getPostVisibilityUseCase: GetPostVisibilityUseCase, + private val postSettingsUtils: PostSettingsUtils, + private val getButtonUiStateUseCase: GetButtonUiStateUseCase, + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper +) : ViewModel() { + private var isStarted = false + + private val _uiState = MutableLiveData>() + val uiState: LiveData> = _uiState + + private val _onActionClicked = MutableLiveData>() + val onActionClicked: LiveData> = _onActionClicked + + private val _onSubmitButtonClicked = MutableLiveData>() + val onSubmitButtonClicked: LiveData> = _onSubmitButtonClicked + + fun start(editPostRepository: EditPostRepository, site: SiteModel) { + if (isStarted) return + isStarted = true + + setupHomeUiState(editPostRepository, site) + } + + private fun setupHomeUiState(editPostRepository: EditPostRepository, site: SiteModel) { + val prepublishingHomeUiStateList = mutableListOf().apply { + add(HeaderUiState(UiStringText(site.name), StringUtils.notNullStr(site.iconUrl))) + + add( + HomeUiState( + actionType = VISIBILITY, + actionResult = getPostVisibilityUseCase.getVisibility(editPostRepository).textRes, + actionClickable = true, + onActionClicked = ::onActionClicked + ) + ) + + if (editPostRepository.status != PostStatus.PRIVATE) { + add( + HomeUiState( + actionType = PUBLISH, + actionResult = editPostRepository.getEditablePost() + ?.let { UiStringText(postSettingsUtils.getPublishDateLabel(it)) }, + actionClickable = true, + onActionClicked = ::onActionClicked + ) + ) + } else { + add( + HomeUiState( + actionType = PUBLISH, + actionResult = editPostRepository.getEditablePost() + ?.let { UiStringText(postSettingsUtils.getPublishDateLabel(it)) }, + actionTypeColor = R.color.prepublishing_action_type_disabled_color, + actionResultColor = R.color.prepublishing_action_result_disabled_color, + actionClickable = false, + onActionClicked = null + ) + ) + } + + if (!editPostRepository.isPage) { + add(HomeUiState( + actionType = TAGS, + actionResult = getPostTagsUseCase.getTags(editPostRepository)?.let { UiStringText(it) } + ?: run { UiStringRes(R.string.prepublishing_nudges_home_tags_not_set) }, + actionClickable = true, + onActionClicked = ::onActionClicked + )) + } + + add(getButtonUiStateUseCase.getUiState(editPostRepository, site) { publishPost -> + analyticsTrackerWrapper.trackPrepublishingNudges(Stat.EDITOR_POST_PUBLISH_NOW_TAPPED) + _onSubmitButtonClicked.postValue(Event(publishPost)) + }) + }.toList() + + _uiState.postValue(prepublishingHomeUiStateList) + } + + private fun onActionClicked(actionType: ActionType) { + _onActionClicked.postValue(Event(actionType)) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingScreenClosedListener.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingScreenClosedListener.kt new file mode 100644 index 000000000000..90f6cdd50875 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingScreenClosedListener.kt @@ -0,0 +1,6 @@ +package org.wordpress.android.ui.posts + +interface PrepublishingScreenClosedListener { + fun onCloseClicked() + fun onBackClicked() +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingTagsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingTagsFragment.kt new file mode 100644 index 000000000000..c471d58da469 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingTagsFragment.kt @@ -0,0 +1,127 @@ +package org.wordpress.android.ui.posts + +import android.content.Context +import android.os.Bundle +import android.view.View +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import kotlinx.android.synthetic.main.fragment_post_settings_tags.* +import kotlinx.android.synthetic.main.prepublishing_toolbar.* +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.posts.EditPostSettingsFragment.EditPostActivityHook +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.ActivityUtils +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import javax.inject.Inject + +class PrepublishingTagsFragment : TagsFragment(), TagsSelectedListener { + private var closeListener: PrepublishingScreenClosedListener? = null + + @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject lateinit var uiHelpers: UiHelpers + @Inject lateinit var analyticsTrackerWrapper: AnalyticsTrackerWrapper + + private lateinit var viewModel: PrepublishingTagsViewModel + + override fun getContentLayout() = R.layout.prepublishing_tags_fragment + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + closeListener = parentFragment as PrepublishingScreenClosedListener + mTagsSelectedListener = this + } + + override fun onDetach() { + super.onDetach() + closeListener = null + } + + override fun getTagsFromEditPostRepositoryOrArguments() = viewModel.getPostTags() + + companion object { + const val TAG = "prepublishing_tags_fragment_tag" + @JvmStatic fun newInstance(site: SiteModel): PrepublishingTagsFragment { + val bundle = Bundle().apply { + putSerializable(WordPress.SITE, site) + } + return PrepublishingTagsFragment().apply { arguments = bundle } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + close_button.setOnClickListener { + trackTagsChangedEvent() + viewModel.onCloseButtonClicked() + } + back_button.setOnClickListener { + trackTagsChangedEvent() + viewModel.onBackButtonClicked() + } + initViewModel() + super.onViewCreated(view, savedInstanceState) + } + + private fun trackTagsChangedEvent() { + if (wereTagsChanged()) { + analyticsTrackerWrapper.trackPrepublishingNudges(Stat.EDITOR_POST_TAGS_CHANGED) + } + } + + private fun initViewModel() { + viewModel = ViewModelProviders.of(this, viewModelFactory) + .get(PrepublishingTagsViewModel::class.java) + + viewModel.dismissBottomSheet.observe(viewLifecycleOwner, Observer { event -> + event?.applyIfNotHandled { + closeListener?.onCloseClicked() + } + }) + + viewModel.dismissKeyboard.observe(viewLifecycleOwner, Observer { event -> + event?.applyIfNotHandled { + ActivityUtils.hideKeyboardForced(tags_edit_text) + } + }) + + viewModel.navigateToHomeScreen.observe(viewLifecycleOwner, Observer { event -> + event?.applyIfNotHandled { + closeListener?.onBackClicked() + } + }) + + viewModel.toolbarTitleUiState.observe(viewLifecycleOwner, Observer { uiString -> + toolbar_title.text = uiHelpers.getTextOfUiString(requireContext(), uiString) + }) + + viewModel.start(getEditPostRepository()) + } + + private fun getEditPostRepository(): EditPostRepository { + val editPostActivityHook = requireNotNull(getEditPostActivityHook()) { "This is possibly null because it's " + + "called during config changes." } + + return editPostActivityHook.editPostRepository + } + + private fun getEditPostActivityHook(): EditPostActivityHook? { + val activity = activity ?: return null + return if (activity is EditPostActivityHook) { + activity + } else { + throw RuntimeException("$activity must implement EditPostActivityHook") + } + } + + override fun onTagsSelected(selectedTags: String) { + viewModel.onTagsSelected(selectedTags) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingTagsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingTagsViewModel.kt new file mode 100644 index 000000000000..3a36505d11a0 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingTagsViewModel.kt @@ -0,0 +1,74 @@ +package org.wordpress.android.ui.posts + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.wordpress.android.R +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.viewmodel.Event +import org.wordpress.android.viewmodel.ScopedViewModel +import javax.inject.Inject +import javax.inject.Named + +private const val THROTTLE_DELAY = 500L + +class PrepublishingTagsViewModel @Inject constructor( + private val getPostTagsUseCase: GetPostTagsUseCase, + private val updatePostTagsUseCase: UpdatePostTagsUseCase, + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher +) : ScopedViewModel(bgDispatcher) { + private var isStarted = false + private lateinit var editPostRepository: EditPostRepository + private var updateTagsJob: Job? = null + + private val _navigateToHomeScreen = MutableLiveData>() + val navigateToHomeScreen: LiveData> = _navigateToHomeScreen + + private val _dismissBottomSheet = MutableLiveData>() + val dismissBottomSheet: LiveData> = _dismissBottomSheet + + private val _dismissKeyboard = MutableLiveData>() + val dismissKeyboard: LiveData> = _dismissKeyboard + + private val _toolbarTitleUiState = MutableLiveData() + val toolbarTitleUiState: LiveData = _toolbarTitleUiState + + fun start(editPostRepository: EditPostRepository) { + if (isStarted) return + isStarted = true + + this.editPostRepository = editPostRepository + setToolbarTitleUiState() + } + + private fun setToolbarTitleUiState() { + _toolbarTitleUiState.postValue(UiStringRes(R.string.prepublishing_nudges_toolbar_title_tags)) + } + + fun onTagsSelected(selectedTags: String) { + updateTagsJob?.cancel() + updateTagsJob = launch(bgDispatcher) { + delay(THROTTLE_DELAY) + updatePostTagsUseCase.updateTags(selectedTags, editPostRepository) + } + } + + fun onCloseButtonClicked() = _dismissBottomSheet.postValue(Event(Unit)) + + fun onBackButtonClicked() { + _dismissKeyboard.postValue(Event(Unit)) + _navigateToHomeScreen.postValue(Event(Unit)) + } + + fun getPostTags() = getPostTagsUseCase.getTags(editPostRepository) + + override fun onCleared() { + super.onCleared() + updateTagsJob?.cancel() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingViewModel.kt new file mode 100644 index 000000000000..8f7b27caa7cc --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingViewModel.kt @@ -0,0 +1,129 @@ +package org.wordpress.android.ui.posts + +import android.os.Bundle +import android.os.Parcelable +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import kotlinx.android.parcel.Parcelize +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.generated.TaxonomyActionBuilder +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.TaxonomyStore.OnTaxonomyChanged +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ActionType +import org.wordpress.android.ui.posts.PrepublishingScreen.HOME +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T +import org.wordpress.android.viewmodel.Event +import javax.inject.Inject + +const val KEY_SCREEN_STATE = "key_screen_state" + +class PrepublishingViewModel @Inject constructor(private val dispatcher: Dispatcher) : ViewModel() { + private var isStarted = false + private lateinit var site: SiteModel + + private val _navigationTarget = MutableLiveData>() + val navigationTarget: LiveData> = _navigationTarget + + private var currentScreen: PrepublishingScreen? = null + + private val _dismissBottomSheet = MutableLiveData>() + val dismissBottomSheet: LiveData> = _dismissBottomSheet + + private val _triggerOnSubmitButtonClickedListener = MutableLiveData>() + val triggerOnSubmitButtonClickedListener: LiveData> = _triggerOnSubmitButtonClickedListener + + init { + dispatcher.register(this) + } + + override fun onCleared() { + super.onCleared() + dispatcher.unregister(this) + } + + fun start( + site: SiteModel, + currentScreenFromSavedState: PrepublishingScreen? + ) { + if (isStarted) return + isStarted = true + + this.site = site + this.currentScreen = currentScreenFromSavedState + + currentScreen?.let { screen -> + navigateToScreen(screen) + } ?: run { + navigateToScreen(HOME) + } + fetchTags() + } + + private fun navigateToScreen(prepublishingScreen: PrepublishingScreen) { + updateNavigationTarget(PrepublishingNavigationTarget(site, prepublishingScreen)) + } + + fun onBackClicked() { + if (currentScreen != HOME) { + currentScreen = HOME + navigateToScreen(currentScreen as PrepublishingScreen) + } else { + _dismissBottomSheet.postValue(Event(Unit)) + } + } + + fun onCloseClicked() { + _dismissBottomSheet.postValue(Event(Unit)) + } + + private fun updateNavigationTarget(target: PrepublishingNavigationTarget) { + _navigationTarget.postValue(Event(target)) + } + + fun writeToBundle(outState: Bundle) { + outState.putParcelable(KEY_SCREEN_STATE, currentScreen) + } + + fun onActionClicked(actionType: ActionType) { + val screen = PrepublishingScreen.valueOf(actionType.name) + currentScreen = screen + navigateToScreen(screen) + } + + fun onSubmitButtonClicked(publishPost: PublishPost) { + onCloseClicked() + _triggerOnSubmitButtonClickedListener.postValue(Event(publishPost)) + } + + /** + * Fetches the tags so that they will be available when the Tags action is clicked + */ + private fun fetchTags() { + dispatcher.dispatch(TaxonomyActionBuilder.newFetchTagsAction(site)) + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + fun onTaxonomyChanged(event: OnTaxonomyChanged) { + if (event.isError) { + AppLog.e(T.POSTS, "An error occurred while updating taxonomy with type: " + event.error.type) + } + } +} + +@Parcelize +enum class PrepublishingScreen : Parcelable { + HOME, + PUBLISH, + VISIBILITY, + TAGS +} + +data class PrepublishingNavigationTarget( + val site: SiteModel, + val targetScreen: PrepublishingScreen +) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishSettingsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishSettingsFragment.kt new file mode 100644 index 000000000000..c71a85c2c506 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishSettingsFragment.kt @@ -0,0 +1,223 @@ +package org.wordpress.android.ui.posts + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context.ALARM_SERVICE +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.provider.CalendarContract +import android.provider.CalendarContract.Events +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.LayoutRes +import androidx.core.app.NotificationManagerCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import kotlinx.android.parcel.Parcelize +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel +import org.wordpress.android.ui.posts.EditPostSettingsFragment.EditPostActivityHook +import org.wordpress.android.ui.posts.PublishSettingsFragmentType.EDIT_POST +import org.wordpress.android.util.AccessibilityUtils +import org.wordpress.android.util.ToastUtils +import org.wordpress.android.util.ToastUtils.Duration.SHORT +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import javax.inject.Inject + +abstract class PublishSettingsFragment : Fragment() { + @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject lateinit var analyticsTrackerWrapper: AnalyticsTrackerWrapper + lateinit var viewModel: PublishSettingsViewModel + + @LayoutRes protected abstract fun getContentLayout(): Int + + protected abstract fun getPublishSettingsFragmentType(): PublishSettingsFragmentType + + protected abstract fun setupContent( + rootView: ViewGroup, + viewModel: PublishSettingsViewModel + ) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val rootView = inflater.inflate(getContentLayout(), container, false) as ViewGroup + val dateAndTime = rootView.findViewById(R.id.publish_time_and_date) + val dateAndTimeContainer = rootView.findViewById(R.id.publish_time_and_date_container) + val publishNotification = rootView.findViewById(R.id.publish_notification) + val publishNotificationTitle = rootView.findViewById(R.id.publish_notification_title) + val publishNotificationContainer = rootView.findViewById(R.id.publish_notification_container) + val addToCalendarContainer = rootView.findViewById(R.id.post_add_to_calendar_container) + val addToCalendar = rootView.findViewById(R.id.post_add_to_calendar) + + AccessibilityUtils.disableHintAnnouncement(dateAndTime) + AccessibilityUtils.disableHintAnnouncement(publishNotification) + + dateAndTimeContainer.setOnClickListener { showPostDateSelectionDialog() } + + setupContent(rootView, viewModel) + + viewModel.onDatePicked.observe(viewLifecycleOwner, Observer { + it?.applyIfNotHandled { + showPostTimeSelectionDialog() + } + }) + viewModel.onPublishedDateChanged.observe(viewLifecycleOwner, Observer { event -> + event.getContentIfNotHandled()?.let { date -> + viewModel.updatePost(date, getPostRepository()) + trackPostScheduled() + } + }) + viewModel.onNotificationTime.observe(viewLifecycleOwner, Observer { + it?.let { notificationTime -> + getPostRepository()?.let { postRepository -> + viewModel.scheduleNotification(postRepository, notificationTime) + } + } + }) + viewModel.onUiModel.observe(viewLifecycleOwner, Observer { + it?.let { uiModel -> + dateAndTime.text = uiModel.publishDateLabel + publishNotificationTitle.isEnabled = uiModel.notificationEnabled + publishNotification.isEnabled = uiModel.notificationEnabled + publishNotificationContainer.isEnabled = uiModel.notificationEnabled + addToCalendar.isEnabled = uiModel.notificationEnabled + addToCalendarContainer.isEnabled = uiModel.notificationEnabled + if (uiModel.notificationEnabled) { + publishNotificationContainer.setOnClickListener { + getPostRepository()?.getPost()?.let { postModel -> + viewModel.onShowDialog(postModel) + } + } + addToCalendarContainer.setOnClickListener { + getPostRepository()?.let { postRepository -> + viewModel.onAddToCalendar(postRepository) + } + } + } else { + publishNotificationContainer.setOnClickListener(null) + addToCalendarContainer.setOnClickListener(null) + } + publishNotification.setText(uiModel.notificationLabel) + publishNotificationContainer.visibility = if (uiModel.notificationVisible) View.VISIBLE else View.GONE + addToCalendarContainer.visibility = if (uiModel.notificationVisible) View.VISIBLE else View.GONE + } + }) + viewModel.onShowNotificationDialog.observe(viewLifecycleOwner, Observer { + it?.getContentIfNotHandled()?.let { notificationTime -> + showNotificationTimeSelectionDialog(notificationTime) + } + }) + viewModel.onToast.observe(viewLifecycleOwner, Observer { + it?.applyIfNotHandled { + ToastUtils.showToast( + context, + this, + SHORT, + Gravity.TOP + ) + } + }) + viewModel.onNotificationAdded.observe(viewLifecycleOwner, Observer { event -> + event?.getContentIfNotHandled()?.let { notification -> + activity?.let { + NotificationManagerCompat.from(it).cancel(notification.id) + val notificationIntent = Intent(it, PublishNotificationReceiver::class.java) + notificationIntent.putExtra(PublishNotificationReceiver.NOTIFICATION_ID, notification.id) + val pendingIntent = PendingIntent.getBroadcast( + it, + notification.id, + notificationIntent, + PendingIntent.FLAG_CANCEL_CURRENT + ) + + val alarmManager = it.getSystemService(ALARM_SERVICE) as AlarmManager + alarmManager.set( + AlarmManager.RTC_WAKEUP, + notification.scheduledTime, + pendingIntent + ) + } + } + }) + viewModel.onAddToCalendar.observe(viewLifecycleOwner, Observer { + it?.getContentIfNotHandled()?.let { calendarEvent -> + val calIntent = Intent(Intent.ACTION_INSERT) + calIntent.data = Events.CONTENT_URI + calIntent.type = "vnd.android.cursor.item/event" + calIntent.putExtra(Events.TITLE, calendarEvent.title) + calIntent.putExtra(Events.DESCRIPTION, calendarEvent.description) + calIntent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, calendarEvent.startTime) + startActivity(calIntent) + } + }) + viewModel.start(getPostRepository()) + return rootView + } + + private fun trackPostScheduled() { + when (getPublishSettingsFragmentType()) { + EDIT_POST -> { + analyticsTrackerWrapper.trackPostSettings(Stat.EDITOR_POST_SCHEDULE_CHANGED) + } + PublishSettingsFragmentType.PREPUBLISHING_NUDGES -> { + analyticsTrackerWrapper.trackPrepublishingNudges(Stat.EDITOR_POST_SCHEDULE_CHANGED) + } + } + } + + private fun showPostDateSelectionDialog() { + if (!isAdded) { + return + } + + val fragment = PostDatePickerDialogFragment.newInstance(getPublishSettingsFragmentType()) + fragment.show(requireActivity().supportFragmentManager, PostDatePickerDialogFragment.TAG) + } + + private fun showPostTimeSelectionDialog() { + if (!isAdded) { + return + } + + val fragment = PostTimePickerDialogFragment.newInstance(getPublishSettingsFragmentType()) + fragment.show(requireActivity().supportFragmentManager, PostTimePickerDialogFragment.TAG) + } + + private fun showNotificationTimeSelectionDialog(schedulingReminderPeriod: SchedulingReminderModel.Period?) { + if (!isAdded) { + return + } + + val fragment = PostNotificationScheduleTimeDialogFragment.newInstance( + schedulingReminderPeriod, + getPublishSettingsFragmentType() + ) + fragment.show(requireActivity().supportFragmentManager, PostNotificationScheduleTimeDialogFragment.TAG) + } + + private fun getPostRepository(): EditPostRepository? { + return getEditPostActivityHook()?.editPostRepository + } + + private fun getEditPostActivityHook(): EditPostActivityHook? { + val activity = activity ?: return null + + return if (activity is EditPostActivityHook) { + activity + } else { + throw RuntimeException("$activity must implement EditPostActivityHook") + } + } +} + +@Parcelize +enum class PublishSettingsFragmentType : Parcelable { + EDIT_POST, + PREPUBLISHING_NUDGES +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishSettingsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishSettingsViewModel.kt new file mode 100644 index 000000000000..b3fc0e324a6d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishSettingsViewModel.kt @@ -0,0 +1,270 @@ +package org.wordpress.android.ui.posts + +import android.text.TextUtils +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.wordpress.android.R +import org.wordpress.android.fluxc.model.PostImmutableModel +import org.wordpress.android.fluxc.model.post.PostStatus +import org.wordpress.android.fluxc.model.post.PostStatus.DRAFT +import org.wordpress.android.fluxc.model.post.PostStatus.PUBLISHED +import org.wordpress.android.fluxc.model.post.PostStatus.SCHEDULED +import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore +import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period +import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.OFF +import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.ONE_HOUR +import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.TEN_MINUTES +import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.WHEN_PUBLISHED +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.ui.posts.EditPostRepository.UpdatePostResult +import org.wordpress.android.util.DateTimeUtils +import org.wordpress.android.util.LocaleManagerWrapper +import org.wordpress.android.viewmodel.Event +import org.wordpress.android.viewmodel.ResourceProvider +import java.util.Calendar + +abstract class PublishSettingsViewModel +constructor( + private val resourceProvider: ResourceProvider, + private val postSettingsUtils: PostSettingsUtils, + private val localeManagerWrapper: LocaleManagerWrapper, + private val postSchedulingNotificationStore: PostSchedulingNotificationStore, + private val siteStore: SiteStore +) : ViewModel() { + var canPublishImmediately: Boolean = false + + var year: Int? = null + private set + var month: Int? = null + private set + var day: Int? = null + private set + var hour: Int? = null + private set + var minute: Int? = null + private set + + private val _onDatePicked = MutableLiveData>() + val onDatePicked: LiveData> = _onDatePicked + private val _onPublishedDateChanged = MutableLiveData>() + val onPublishedDateChanged: LiveData> = _onPublishedDateChanged + private val _onPostStatusChanged = MutableLiveData() + val onPostStatusChanged: LiveData = _onPostStatusChanged + private val _onUiModel = MutableLiveData() + val onUiModel: LiveData = _onUiModel + private val _onToast = MutableLiveData>() + val onToast: LiveData> = _onToast + private val _onShowNotificationDialog = MutableLiveData>() + val onShowNotificationDialog: LiveData> = _onShowNotificationDialog + private val _onNotificationTime = MutableLiveData() + val onNotificationTime: LiveData = _onNotificationTime + private val _onNotificationAdded = MutableLiveData>() + val onNotificationAdded: LiveData> = _onNotificationAdded + private val _onAddToCalendar = MutableLiveData>() + val onAddToCalendar: LiveData> = _onAddToCalendar + + open fun start(postRepository: EditPostRepository?) { + val startCalendar = postRepository?.let { getCurrentPublishDateAsCalendar(it) } + ?: localeManagerWrapper.getCurrentCalendar() + updateDateAndTimeFromCalendar(startCalendar) + onPostStatusChanged(postRepository?.getPost()) + } + + fun onPostStatusChanged(postModel: PostImmutableModel?) { + canPublishImmediately = postModel?.let { + PostUtils.shouldPublishImmediatelyOptionBeAvailable( + it.status + ) + } ?: false + updateUiModel(postModel = postModel) + } + + fun publishNow() { + val currentCalendar = localeManagerWrapper.getCurrentCalendar() + updateDateAndTimeFromCalendar(currentCalendar) + _onPublishedDateChanged.postValue(Event(currentCalendar)) + } + + fun onTimeSelected(selectedHour: Int, selectedMinute: Int) { + this.hour = selectedHour + this.minute = selectedMinute + val calendar = localeManagerWrapper.getCurrentCalendar() + calendar.set(year!!, month!!, day!!, hour!!, minute!!) + _onPublishedDateChanged.postValue(Event(calendar)) + } + + fun onDateSelected(year: Int, month: Int, dayOfMonth: Int) { + this.year = year + this.month = month + this.day = dayOfMonth + _onDatePicked.postValue(Event(Unit)) + } + + fun onShowDialog(postModel: PostImmutableModel) { + if (areNotificationsEnabled(postModel)) { + val currentPeriod = postSchedulingNotificationStore.getSchedulingReminderPeriod(postModel.id) + _onShowNotificationDialog.postValue(Event(currentPeriod)) + } else { + _onToast.postValue(Event(resourceProvider.getString(R.string.post_notification_error))) + } + } + + fun updatePost(updatedDate: Calendar, postRepository: EditPostRepository?) { + postRepository?.updateAsync({ postModel -> + val dateCreated = DateTimeUtils.iso8601FromDate(updatedDate.time) + postModel.setDateCreated(dateCreated) + val initialPostStatus = postRepository.status + val isPublishDateInTheFuture = PostUtils.isPublishDateInTheFuture(dateCreated) + var finalPostStatus = initialPostStatus + if (initialPostStatus == DRAFT && isPublishDateInTheFuture) { + // The previous logic was setting the status twice, once from draft to published and when the user + // picked the time, it set it from published to scheduled. This is now done in one step. + finalPostStatus = SCHEDULED + } else if (initialPostStatus == PUBLISHED && postRepository.isLocalDraft) { + // if user was changing dates for a local draft (not saved yet), only way to have it set to PUBLISH + // is by running into the if case above. So, if they're updating the date again by calling + // `updatePublishDate()`, get it back to DRAFT. + finalPostStatus = DRAFT + } else if (initialPostStatus == SCHEDULED && !isPublishDateInTheFuture) { + // if this is a SCHEDULED post and the user is trying to Back-date it now, let's update it to DRAFT. + // The other option was to make it published immediately but, let the user actively do that rather than + // having the app be smart about it - we don't want to accidentally publish a post. + finalPostStatus = DRAFT + // show toast only once, when time is shown + _onToast.postValue(Event(resourceProvider.getString(R.string.editor_post_converted_back_to_draft))) + } + postModel.setStatus(finalPostStatus.toString()) + _onPostStatusChanged.postValue(finalPostStatus) + val scheduledTime = postSchedulingNotificationStore.getSchedulingReminderPeriod(postRepository.id) + updateNotifications(postRepository, scheduledTime) + true + }, onCompleted = { postModel, result -> + if (result == UpdatePostResult.Updated) { + updateUiModel(postModel = postModel) + } + }) + } + + fun updateUiModel(postModel: PostImmutableModel?) { + if (postModel != null) { + val notificationTime = postSchedulingNotificationStore.getSchedulingReminderPeriod(postModel.id) + val publishDateLabel = postSettingsUtils.getPublishDateLabel(postModel) + val now = localeManagerWrapper.getCurrentCalendar().timeInMillis - 10000 + val dateCreated = (DateTimeUtils.dateFromIso8601(postModel.dateCreated) + ?: localeManagerWrapper.getCurrentCalendar().time).time + val enableNotification = areNotificationsEnabled(postModel) + val showNotification = dateCreated > now + val notificationLabel = if (enableNotification && showNotification) { + notificationTime.toLabel() + } else { + R.string.post_notification_off + } + _onUiModel.value = PublishUiModel( + publishDateLabel, + notificationLabel = notificationLabel, + notificationEnabled = enableNotification, + notificationVisible = showNotification + ) + } else { + _onUiModel.value = PublishUiModel(resourceProvider.getString(R.string.immediately)) + } + } + + fun onNotificationCreated(scheduleTime: Period?) { + _onNotificationTime.value = scheduleTime + } + + fun scheduleNotification(postRepository: EditPostRepository, notificationTime: Period) { + updateNotifications(postRepository, notificationTime) + updateUiModel(postRepository.getPost()) + } + + fun onAddToCalendar(postRepository: EditPostRepository) { + val startTime = DateTimeUtils.dateFromIso8601(postRepository.dateCreated).time + val site = siteStore.getSiteByLocalId(postRepository.localSiteId) + val title = resourceProvider.getString( + R.string.calendar_scheduled_post_title, + postRepository.title + ) + val description = resourceProvider.getString( + R.string.calendar_scheduled_post_description, + postRepository.title, + site.name ?: site.url, + postRepository.link + ) + _onAddToCalendar.value = Event(CalendarEvent(title, description, startTime)) + } + + private fun getCurrentPublishDateAsCalendar(postRepository: EditPostRepository): Calendar { + val calendar = localeManagerWrapper.getCurrentCalendar() + val dateCreated = postRepository.dateCreated + // Set the currently selected time if available + if (!TextUtils.isEmpty(dateCreated)) { + calendar.time = DateTimeUtils.dateFromIso8601(dateCreated) + calendar.timeZone = localeManagerWrapper.getTimeZone() + } + return calendar + } + + private fun updateNotifications( + postRepository: EditPostRepository, + schedulingReminderPeriod: Period = OFF + ) { + postSchedulingNotificationStore.deleteSchedulingReminders(postRepository.id) + if (schedulingReminderPeriod != OFF) { + val notificationId = postSchedulingNotificationStore.schedule(postRepository.id, schedulingReminderPeriod) + val scheduledCalendar = localeManagerWrapper.getCurrentCalendar().apply { + timeInMillis = System.currentTimeMillis() + time = DateTimeUtils.dateFromIso8601(postRepository.dateCreated) + val scheduledMinutes = when (schedulingReminderPeriod) { + ONE_HOUR -> -60 + TEN_MINUTES -> -10 + WHEN_PUBLISHED -> 0 + OFF -> return + } + add(Calendar.MINUTE, scheduledMinutes) + } + if (scheduledCalendar.after(localeManagerWrapper.getCurrentCalendar())) { + notificationId?.let { + _onNotificationAdded.postValue(Event(Notification(notificationId, scheduledCalendar.timeInMillis))) + } + } + } + } + + private fun areNotificationsEnabled(postModel: PostImmutableModel): Boolean { + val futureTime = localeManagerWrapper.getCurrentCalendar().timeInMillis + 6000 + val dateCreated = (DateTimeUtils.dateFromIso8601(postModel.dateCreated) + ?: localeManagerWrapper.getCurrentCalendar().time).time + return dateCreated > futureTime + } + + private fun Period.toLabel(): Int { + return when (this) { + OFF -> R.string.post_notification_off + ONE_HOUR -> R.string.post_notification_one_hour_before + TEN_MINUTES -> R.string.post_notification_ten_minutes_before + WHEN_PUBLISHED -> R.string.post_notification_when_published + } + } + + private fun updateDateAndTimeFromCalendar(startCalendar: Calendar) { + year = startCalendar.get(Calendar.YEAR) + month = startCalendar.get(Calendar.MONTH) + day = startCalendar.get(Calendar.DAY_OF_MONTH) + hour = startCalendar.get(Calendar.HOUR_OF_DAY) + minute = startCalendar.get(Calendar.MINUTE) + } + + data class PublishUiModel( + val publishDateLabel: String, + val notificationLabel: Int = R.string.post_notification_off, + val notificationEnabled: Boolean = false, + val notificationVisible: Boolean = true + ) + + data class Notification(val id: Int, val scheduledTime: Long) + + data class CalendarEvent(val title: String, val description: String, val startTime: Long) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/TagSelectedListener.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/TagSelectedListener.java new file mode 100644 index 000000000000..2aff5e86757c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/TagSelectedListener.java @@ -0,0 +1,7 @@ +package org.wordpress.android.ui.posts; + +import androidx.annotation.NonNull; + +interface TagSelectedListener { + void onTagSelected(@NonNull String selectedTag); +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/TagsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/TagsFragment.java new file mode 100644 index 000000000000..dad2bdf0f9dc --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/TagsFragment.java @@ -0,0 +1,208 @@ +package org.wordpress.android.ui.posts; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.apache.commons.text.StringEscapeUtils; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.fluxc.Dispatcher; +import org.wordpress.android.fluxc.model.SiteModel; +import org.wordpress.android.fluxc.store.TaxonomyStore; +import org.wordpress.android.fluxc.store.TaxonomyStore.OnTaxonomyChanged; +import org.wordpress.android.util.ActivityUtils; + +import javax.inject.Inject; + +import static org.wordpress.android.ui.posts.PostSettingsTagsActivity.KEY_TAGS; + +public abstract class TagsFragment extends Fragment implements TextWatcher, View.OnKeyListener, TagSelectedListener { + private SiteModel mSite; + + private EditText mTagsEditText; + private TagsRecyclerViewAdapter mAdapter; + + @Inject Dispatcher mDispatcher; + @Inject TaxonomyStore mTaxonomyStore; + + private String mTags; + + TagsSelectedListener mTagsSelectedListener; + + public TagsFragment() { + } + + protected abstract @LayoutRes int getContentLayout(); + + protected abstract String getTagsFromEditPostRepositoryOrArguments(); + + @Override public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments() != null) { + mSite = (SiteModel) getArguments().getSerializable(WordPress.SITE); + mTags = getArguments().getString(KEY_TAGS); + + if (mSite == null) { + throw new IllegalStateException("Required argument mSite is missing."); + } + } + } + + @Override public void onDetach() { + super.onDetach(); + mTagsSelectedListener = null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(getContentLayout(), container, false); + } + + @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.tags_suggestion_list); + recyclerView.setHasFixedSize(true); + recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity())); + + mAdapter = new TagsRecyclerViewAdapter(requireActivity(), this); + mAdapter.setAllTags(mTaxonomyStore.getTagsForSite(mSite)); + recyclerView.setAdapter(mAdapter); + + mTagsEditText = (EditText) view.findViewById(R.id.tags_edit_text); + mTagsEditText.setOnKeyListener(this); + mTagsEditText.requestFocus(); + ActivityUtils.showKeyboard(mTagsEditText); + mTagsEditText.post(() -> mTagsEditText.addTextChangedListener(TagsFragment.this)); + + loadTags(); + + if (!TextUtils.isEmpty(mTags)) { + // add a , at the end so the user can start typing a new tag + mTags += ","; + mTags = StringEscapeUtils.unescapeHtml4(mTags); + mTagsEditText.setText(mTags); + mTagsEditText.setSelection(mTagsEditText.length()); + } + filterListForCurrentText(); + } + + private void loadTags() { + mTags = getTagsFromEditPostRepositoryOrArguments(); + } + + @Override + public void onStart() { + super.onStart(); + mDispatcher.register(this); + } + + @Override + public void onStop() { + mDispatcher.unregister(this); + super.onStop(); + } + + @Override + public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { + if ((keyEvent.getAction() == KeyEvent.ACTION_DOWN) + && (keyCode == KeyEvent.KEYCODE_ENTER)) { + // Since we don't allow new lines, we should add comma on "enter" to separate the tags + String currentText = mTagsEditText.getText().toString(); + if (!currentText.isEmpty() && !currentText.endsWith(",")) { + mTagsEditText.setText(currentText.concat(",")); + mTagsEditText.setSelection(mTagsEditText.length()); + } + return true; + } + return false; + } + + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + // No-op + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + filterListForCurrentText(); + mTagsSelectedListener.onTagsSelected(charSequence.toString()); + } + + @Override + public void afterTextChanged(Editable editable) { + // No-op + } + + // Find the text after the last occurrence of "," and filter with it + private void filterListForCurrentText() { + String text = mTagsEditText.getText().toString(); + int endIndex = text.lastIndexOf(","); + if (endIndex == -1) { + mAdapter.filter(text); + } else { + String textToFilter = text.substring(endIndex + 1).trim(); + mAdapter.filter(textToFilter); + } + } + + public void onTagSelected(@NonNull String selectedTag) { + String text = mTagsEditText.getText().toString(); + String updatedText; + int endIndex = text.lastIndexOf(","); + if (endIndex == -1) { + // no "," found, replace the current text with the selectedTag + updatedText = selectedTag; + } else { + // there are multiple tags already, only update the text after the last "," + updatedText = text.substring(0, endIndex + 1) + selectedTag; + } + updatedText += ","; + updatedText = StringEscapeUtils.unescapeHtml4(updatedText); + mTagsEditText.setText(updatedText); + mTagsEditText.setSelection(mTagsEditText.length()); + } + + boolean wereTagsChanged() { + if (mTags != null) { + return !mTags.equals(mTagsEditText.getText().toString()); + } else { + return !mTagsEditText.getText().toString().isEmpty(); + } + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onTaxonomyChanged(OnTaxonomyChanged event) { + switch (event.causeOfChange) { + case FETCH_TAGS: + mAdapter.setAllTags(mTaxonomyStore.getTagsForSite(mSite)); + filterListForCurrentText(); + break; + } + } + + void closeKeyboard() { + ActivityUtils.hideKeyboardForced(mTagsEditText); + } +} + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/TagsRecyclerViewAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/TagsRecyclerViewAdapter.java new file mode 100644 index 000000000000..c0cb91196686 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/TagsRecyclerViewAdapter.java @@ -0,0 +1,102 @@ +package org.wordpress.android.ui.posts; + +import android.app.Activity; +import android.content.Context; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import org.apache.commons.text.StringEscapeUtils; +import org.wordpress.android.R; +import org.wordpress.android.fluxc.model.TermModel; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class TagsRecyclerViewAdapter extends RecyclerView.Adapter { + private List mAllTags; + private List mFilteredTags; + private Context mContext; + private TagSelectedListener mTagSelectedListener; + + TagsRecyclerViewAdapter(Context context, TagSelectedListener tagSelectedListener) { + mContext = context; + mTagSelectedListener = tagSelectedListener; + mFilteredTags = new ArrayList<>(); + } + + @Override + public TagViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.tags_list_row, parent, false); + return new TagViewHolder(view); + } + + @Override + public void onBindViewHolder(final TagViewHolder holder, int position) { + // Guard against mFilteredTags getting altered in another thread + if (mFilteredTags.size() <= position) { + return; + } + String tag = StringEscapeUtils.unescapeHtml4(mFilteredTags.get(position).getName()); + holder.mNameTextView.setText(tag); + } + + @Override + public int getItemCount() { + return mFilteredTags.size(); + } + + void setAllTags(List allTags) { + mAllTags = allTags; + } + + public void filter(final String text) { + final List allTags = mAllTags; + new Thread(new Runnable() { + @Override + public void run() { + final List filteredTags = new ArrayList<>(); + if (TextUtils.isEmpty(text)) { + filteredTags.addAll(allTags); + } else { + for (TermModel tag : allTags) { + if (tag.getName().toLowerCase(Locale.getDefault()) + .contains(text.toLowerCase(Locale.getDefault()))) { + filteredTags.add(tag); + } + } + } + + ((Activity) mContext).runOnUiThread(new Runnable() { + @Override + public void run() { + mFilteredTags = filteredTags; + notifyDataSetChanged(); + } + }); + } + }).start(); + } + + class TagViewHolder extends RecyclerView.ViewHolder { + private final TextView mNameTextView; + + TagViewHolder(View view) { + super(view); + mNameTextView = (TextView) view.findViewById(R.id.tag_name); + RelativeLayout layout = (RelativeLayout) view.findViewById(R.id.tags_list_row_container); + layout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mTagSelectedListener.onTagSelected(mNameTextView.getText().toString()); + } + }); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/TagsSelectedListener.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/TagsSelectedListener.java new file mode 100644 index 000000000000..87d8863dcf68 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/TagsSelectedListener.java @@ -0,0 +1,7 @@ +package org.wordpress.android.ui.posts; + +import androidx.annotation.NonNull; + +interface TagsSelectedListener { + void onTagsSelected(@NonNull String selectedTags); +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/UpdatePostTagsUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/UpdatePostTagsUseCase.kt new file mode 100644 index 000000000000..7940d1862f5a --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/UpdatePostTagsUseCase.kt @@ -0,0 +1,20 @@ +package org.wordpress.android.ui.posts + +import android.text.TextUtils +import dagger.Reusable +import javax.inject.Inject + +@Reusable +class UpdatePostTagsUseCase @Inject constructor() { + fun updateTags(selectedTags: String?, editPostRepository: EditPostRepository) { + editPostRepository.updateAsync({ postModel -> + if (selectedTags != null && !TextUtils.isEmpty(selectedTags)) { + val tags: String = selectedTags.replace("\n", " ") + postModel.setTagNameList(TextUtils.split(tags, ",").toList()) + } else { + postModel.setTagNameList(ArrayList()) + } + true + }) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorActionsProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorActionsProvider.kt index ecb59526984a..f50b849c2905 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorActionsProvider.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/EditorActionsProvider.kt @@ -4,11 +4,12 @@ import androidx.annotation.StringRes import dagger.Reusable import org.wordpress.android.R import org.wordpress.android.fluxc.model.post.PostStatus -import org.wordpress.android.util.CrashLoggingUtilsWrapper +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T import javax.inject.Inject @Reusable -class EditorActionsProvider @Inject constructor(private val remoteLoggingUtils: CrashLoggingUtilsWrapper) { +class EditorActionsProvider @Inject constructor() { fun getPrimaryAction(postStatus: PostStatus, userCanPublish: Boolean): PrimaryEditorAction { return if (userCanPublish) { when (postStatus) { @@ -24,23 +25,15 @@ class EditorActionsProvider @Inject constructor(private val remoteLoggingUtils: PostStatus.PENDING, PostStatus.UNKNOWN -> PrimaryEditorAction.SUBMIT_FOR_REVIEW PostStatus.TRASHED -> { - // TODO if this log doesn't appear in Sentry, we should start throwing IllegalStateException - // instead of returning a valid action. - remoteLoggingUtils.log( - "User shouldn't be able to open a trashed post in an editor " + - "without publishing rights." - ) + AppLog.e(T.EDITOR, "User shouldn't be able to open a trashed post in an editor " + + "without publishing rights.") PrimaryEditorAction.SAVE } PostStatus.PUBLISHED, PostStatus.SCHEDULED, PostStatus.PRIVATE -> { - // TODO if this log doesn't appear in Sentry, we should start throwing IllegalStateException - // instead of returning a valid action. - remoteLoggingUtils.log( - "User shouldn't be able to open a public ($postStatus) post in an editor " + - "without publishing rights." - ) + AppLog.e(T.EDITOR, "User shouldn't be able to open a public ($postStatus) post in an editor " + + "without publishing rights.") PrimaryEditorAction.SUBMIT_FOR_REVIEW } } @@ -65,23 +58,15 @@ class EditorActionsProvider @Inject constructor(private val remoteLoggingUtils: PostStatus.PENDING, PostStatus.UNKNOWN -> SecondaryEditorAction.NONE PostStatus.TRASHED -> { - // TODO if this log doesn't appear in Sentry, we should start throwing IllegalStateException - // instead of returning a valid action. - remoteLoggingUtils.log( - "User shouldn't be able to open a trashed post in an editor " + - "without publishing rights." - ) + AppLog.e(T.EDITOR, "User shouldn't be able to open a trashed post in an editor " + + "without publishing rights.") SecondaryEditorAction.SAVE_AS_DRAFT } PostStatus.PUBLISHED, PostStatus.SCHEDULED, PostStatus.PRIVATE -> { - // TODO if this log doesn't appear in Sentry, we should start throwing IllegalStateException - // instead of returning a valid action. - remoteLoggingUtils.log( - "User shouldn't be able to open a public ($postStatus) post in an editor " + - "without publishing rights." - ) + AppLog.e(T.EDITOR, "User shouldn't be able to open a public ($postStatus) post in an editor " + + "without publishing rights.") SecondaryEditorAction.NONE } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/CopyMediaToAppStorageUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/CopyMediaToAppStorageUseCase.kt index ea2d4c5ff5c1..2fe6c8246314 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/CopyMediaToAppStorageUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/CopyMediaToAppStorageUseCase.kt @@ -6,9 +6,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.util.AppLog -import org.wordpress.android.util.AppLog.T.MEDIA import org.wordpress.android.util.AppLog.T.UTILS -import org.wordpress.android.util.CrashLoggingUtils import org.wordpress.android.util.MediaUtilsWrapper import javax.inject.Inject import javax.inject.Named @@ -50,7 +48,6 @@ class CopyMediaToAppStorageUseCase @Inject constructor( // Ref: https://github.com/wordpress-mobile/WordPress-Android/issues/5823 val errorMessage = "Can't download the image at: $mediaUri See issue #5823" AppLog.e(UTILS, errorMessage, e) - CrashLoggingUtils.logException(e, MEDIA, errorMessage) null } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/EditorMedia.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/EditorMedia.kt index e885e5b411b8..9259f79b63b9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/EditorMedia.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/EditorMedia.kt @@ -8,9 +8,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.analytics.AnalyticsTracker.Stat.EDITOR_UPLOAD_MEDIA_FAILED import org.wordpress.android.editor.EditorMediaUploadListener import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.generated.MediaActionBuilder @@ -21,6 +23,8 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.MediaStore import org.wordpress.android.fluxc.store.MediaStore.CancelMediaPayload import org.wordpress.android.fluxc.store.MediaStore.FetchMediaListPayload +import org.wordpress.android.fluxc.store.MediaStore.MediaError +import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.posts.EditPostActivity.OnPostUpdatedFromUIListener @@ -37,6 +41,8 @@ import org.wordpress.android.util.MediaUtilsWrapper import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.StringUtils import org.wordpress.android.util.ToastUtils.Duration +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper import org.wordpress.android.util.helpers.MediaFile import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.SingleLiveEvent @@ -65,7 +71,10 @@ class EditorMedia @Inject constructor( private val cleanUpMediaToPostAssociationUseCase: CleanUpMediaToPostAssociationUseCase, private val removeMediaUseCase: RemoveMediaUseCase, private val reattachUploadingMediaUseCase: ReattachUploadingMediaUseCase, - @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher + private val analyticsUtilsWrapper: AnalyticsUtilsWrapper, + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, + @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher ) : CoroutineScope { // region Fields private var job: Job = Job() @@ -294,6 +303,18 @@ class EditorMedia @Inject constructor( } } + fun onMediaUploadError(listener: EditorMediaUploadListener, media: MediaModel, error: MediaError) = launch { + val properties: Map = withContext(bgDispatcher) { + analyticsUtilsWrapper + .getMediaProperties(media.isVideo, null, media.filePath) + .also { + it["error_type"] = error.type.name + } + } + analyticsTrackerWrapper.track(EDITOR_UPLOAD_MEDIA_FAILED, properties) + listener.onMediaUploadFailed(media.id.toString()) + } + enum class AddExistingMediaSource { WP_MEDIA_LIBRARY, STOCK_PHOTO_LIBRARY diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/GetMediaModelUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/GetMediaModelUseCase.kt index 43118461a670..97b82a50f20b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/GetMediaModelUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/media/GetMediaModelUseCase.kt @@ -8,6 +8,7 @@ import org.wordpress.android.fluxc.model.MediaModel import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.MediaStore import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.utils.AuthenticationUtils import org.wordpress.android.util.AppLog import org.wordpress.android.util.FileProvider import org.wordpress.android.util.FluxCUtilsWrapper @@ -24,6 +25,7 @@ class GetMediaModelUseCase @Inject constructor( private val mediaUtilsWrapper: MediaUtilsWrapper, private val mediaStore: MediaStore, private val fileProvider: FileProvider, + private val authenticationUtils: AuthenticationUtils, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher ) { suspend fun loadMediaByLocalId(mediaModelLocalIds: Iterable): List { @@ -106,7 +108,7 @@ class GetMediaModelUseCase @Inject constructor( private fun createVideoThumbnail(uri: Uri): String? { val path = mediaUtilsWrapper.getRealPathFromURI(uri) - return path?.let { mediaUtilsWrapper.getVideoThumbnail(it) } + return path?.let { mediaUtilsWrapper.getVideoThumbnail(it, authenticationUtils.getAuthHeaders(it)) } } private fun verifyFileExists(uri: Uri): Boolean { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java index f5cc3e18b6bc..cdb63163ad13 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java @@ -1,5 +1,6 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.jsoup.nodes.Document; @@ -50,7 +51,8 @@ public CoverBlockProcessor(String localId, MediaFile mediaFile, } @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { - if (jsonAttributes.get("id").getAsInt() == Integer.parseInt(mLocalId, 10)) { + JsonElement id = jsonAttributes.get("id"); + if (id != null && id.getAsInt() == Integer.parseInt(mLocalId, 10)) { jsonAttributes.addProperty("id", Integer.parseInt(mRemoteId, 10)); jsonAttributes.addProperty("url", mRemoteUrl); return true; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java index 59d7f8363036..9a03593347c0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java @@ -68,6 +68,9 @@ public GalleryBlockProcessor(String localId, MediaFile mediaFile, String siteUrl @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { JsonArray ids = jsonAttributes.getAsJsonArray("ids"); + if (ids == null) { + return false; + } JsonElement linkTo = jsonAttributes.get("linkTo"); if (linkTo != null) { mLinkTo = linkTo.getAsString(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java index fc2dd197060f..c6edb177b121 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java @@ -1,5 +1,6 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.jsoup.nodes.Document; @@ -31,7 +32,8 @@ public ImageBlockProcessor(String localId, MediaFile mediaFile) { } @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { - if (jsonAttributes.get("id").getAsString().equals(mLocalId)) { + JsonElement id = jsonAttributes.get("id"); + if (id != null && id.getAsString().equals(mLocalId)) { jsonAttributes.addProperty("id", Integer.parseInt(mRemoteId)); return true; } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java index 558eaa12536d..4b0ab2a59acd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java @@ -1,5 +1,6 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.jsoup.nodes.Document; @@ -44,7 +45,8 @@ public MediaTextBlockProcessor(String localId, MediaFile mediaFile) { } @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { - if (jsonAttributes.get("mediaId").getAsString().equals(mLocalId)) { + JsonElement id = jsonAttributes.get("mediaId"); + if (id != null && id.getAsString().equals(mLocalId)) { jsonAttributes.addProperty("mediaId", Integer.parseInt(mRemoteId)); return true; } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java index ae91d130dbdf..e37e84b6253c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java @@ -1,5 +1,6 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.jsoup.nodes.Document; @@ -28,7 +29,8 @@ public VideoBlockProcessor(String localId, MediaFile mediaFile) { } @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) { - if (jsonAttributes.get("id").getAsString().equals(mLocalId)) { + JsonElement id = jsonAttributes.get("id"); + if (id != null && id.getAsString().equals(mLocalId)) { jsonAttributes.addProperty("id", Integer.parseInt(mRemoteId)); return true; } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetListener.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetListener.kt new file mode 100644 index 000000000000..0d5ac5f79d94 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetListener.kt @@ -0,0 +1,7 @@ +package org.wordpress.android.ui.posts.prepublishing + +import org.wordpress.android.ui.posts.PublishPost + +interface PrepublishingBottomSheetListener { + fun onSubmitButtonClicked(publishPost: PublishPost) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingPublishSettingsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingPublishSettingsFragment.kt new file mode 100644 index 000000000000..f60a9829c741 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingPublishSettingsFragment.kt @@ -0,0 +1,77 @@ +package org.wordpress.android.ui.posts.prepublishing + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.ui.posts.PrepublishingScreenClosedListener +import org.wordpress.android.ui.posts.PublishSettingsFragment +import org.wordpress.android.ui.posts.PublishSettingsFragmentType.PREPUBLISHING_NUDGES +import org.wordpress.android.ui.posts.PublishSettingsViewModel +import org.wordpress.android.ui.utils.UiHelpers +import javax.inject.Inject + +class PrepublishingPublishSettingsFragment : PublishSettingsFragment() { + @Inject lateinit var uiHelpers: UiHelpers + private var closeListener: PrepublishingScreenClosedListener? = null + + override fun getContentLayout() = R.layout.prepublishing_published_settings_fragment + override fun getPublishSettingsFragmentType() = PREPUBLISHING_NUDGES + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().applicationContext as WordPress).component().inject(this) + viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory) + .get(PrepublishingPublishSettingsViewModel::class.java) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + closeListener = parentFragment as PrepublishingScreenClosedListener + } + + override fun onDetach() { + super.onDetach() + closeListener = null + } + + override fun setupContent(rootView: ViewGroup, viewModel: PublishSettingsViewModel) { + val closeButton = rootView.findViewById(R.id.close_button) + val backButton = rootView.findViewById(R.id.back_button) + val toolbarTitle = rootView.findViewById(R.id.toolbar_title) + + (viewModel as PrepublishingPublishSettingsViewModel).let { + closeButton.setOnClickListener { viewModel.onCloseButtonClicked() } + backButton.setOnClickListener { viewModel.onBackButtonClicked() } + + viewModel.dismissBottomSheet.observe(this, Observer { event -> + event?.applyIfNotHandled { + closeListener?.onCloseClicked() + } + }) + + viewModel.navigateToHomeScreen.observe(this, Observer { event -> + event?.applyIfNotHandled { + closeListener?.onBackClicked() + } + }) + + viewModel.updateToolbarTitle.observe(this, Observer { uiString -> + toolbarTitle.text = uiHelpers.getTextOfUiString( + requireContext(), + uiString + ) + }) + } + } + + companion object { + const val TAG = "prepublishing_publish_settings_fragment_tag" + fun newInstance() = PrepublishingPublishSettingsFragment() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingPublishSettingsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingPublishSettingsViewModel.kt new file mode 100644 index 000000000000..85a97bcfd03f --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingPublishSettingsViewModel.kt @@ -0,0 +1,54 @@ +package org.wordpress.android.ui.posts.prepublishing + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.wordpress.android.R +import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.PostSettingsUtils +import org.wordpress.android.ui.posts.PublishSettingsViewModel +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.util.LocaleManagerWrapper +import org.wordpress.android.viewmodel.Event +import org.wordpress.android.viewmodel.ResourceProvider +import javax.inject.Inject + +class PrepublishingPublishSettingsViewModel @Inject constructor( + resourceProvider: ResourceProvider, + postSettingsUtils: PostSettingsUtils, + localeManagerWrapper: LocaleManagerWrapper, + postSchedulingNotificationStore: PostSchedulingNotificationStore, + siteStore: SiteStore +) : PublishSettingsViewModel( + resourceProvider, + postSettingsUtils, + localeManagerWrapper, + postSchedulingNotificationStore, + siteStore +) { + private val _navigateToHomeScreen = MutableLiveData>() + val navigateToHomeScreen: LiveData> = _navigateToHomeScreen + + private val _dismissBottomSheet = MutableLiveData>() + val dismissBottomSheet: LiveData> = _dismissBottomSheet + + private val _updateToolbarTitle = MutableLiveData() + val updateToolbarTitle: LiveData = _updateToolbarTitle + + override fun start(postRepository: EditPostRepository?) { + super.start(postRepository) + setToolbarTitle() + } + + private fun setToolbarTitle() { + _updateToolbarTitle.postValue(UiStringRes(R.string.prepublishing_nudges_toolbar_title_publish)) + } + + fun onCloseButtonClicked() = _dismissBottomSheet.postValue(Event(Unit)) + + fun onBackButtonClicked() { + _navigateToHomeScreen.postValue(Event(Unit)) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/usecases/GetButtonUiStateUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/usecases/GetButtonUiStateUseCase.kt new file mode 100644 index 000000000000..048a4ab0256a --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/usecases/GetButtonUiStateUseCase.kt @@ -0,0 +1,39 @@ +package org.wordpress.android.ui.posts.prepublishing.home.usecases + +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ButtonUiState +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ButtonUiState.PublishButtonUiState +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ButtonUiState.SaveButtonUiState +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ButtonUiState.ScheduleButtonUiState +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ButtonUiState.SubmitButtonUiState +import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ButtonUiState.UpdateButtonUiState +import org.wordpress.android.ui.posts.PublishPost +import org.wordpress.android.ui.posts.editor.EditorActionsProvider +import org.wordpress.android.ui.posts.editor.PrimaryEditorAction +import org.wordpress.android.ui.uploads.UploadUtilsWrapper +import javax.inject.Inject + +class GetButtonUiStateUseCase @Inject constructor( + private val editorActionsProvider: EditorActionsProvider, + private val uploadUtilsWrapper: UploadUtilsWrapper +) { + fun getUiState( + editPostRepository: EditPostRepository, + site: SiteModel, + onButtonClicked: (PublishPost) -> Unit + ): ButtonUiState { + val editorAction = editorActionsProvider.getPrimaryAction( + editPostRepository.status, + uploadUtilsWrapper.userCanPublish(site) + ) + + return when (editorAction) { + PrimaryEditorAction.PUBLISH_NOW -> PublishButtonUiState(onButtonClicked) + PrimaryEditorAction.SCHEDULE -> ScheduleButtonUiState(onButtonClicked) + PrimaryEditorAction.UPDATE -> UpdateButtonUiState(onButtonClicked) + PrimaryEditorAction.SUBMIT_FOR_REVIEW -> SubmitButtonUiState(onButtonClicked) + PrimaryEditorAction.SAVE -> SaveButtonUiState(onButtonClicked) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/usecases/PublishPostImmediatelyUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/usecases/PublishPostImmediatelyUseCase.kt new file mode 100644 index 000000000000..b343bc68f3b8 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/usecases/PublishPostImmediatelyUseCase.kt @@ -0,0 +1,29 @@ +package org.wordpress.android.ui.posts.prepublishing.home.usecases + +import org.wordpress.android.fluxc.model.PostModel +import org.wordpress.android.fluxc.model.post.PostStatus +import org.wordpress.android.fluxc.model.post.PostStatus.SCHEDULED +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.util.DateTimeUtilsWrapper +import javax.inject.Inject + +class PublishPostImmediatelyUseCase @Inject constructor(private val dateTimeUtilsWrapper: DateTimeUtilsWrapper) { + fun updatePostToPublishImmediately( + editPostRepository: EditPostRepository, + isNewPost: Boolean + ) { + editPostRepository.updateAsync({ postModel: PostModel -> + if (postModel.status == SCHEDULED.toString()) { + postModel.setDateCreated(dateTimeUtilsWrapper.currentTimeInIso8601()) + } + // when the post is a Draft, Publish Now is shown as the Primary Action but if it's already Published then + // Update Now is shown. + if (isNewPost) { + postModel.setStatus(PostStatus.DRAFT.toString()) + } else { + postModel.setStatus(PostStatus.PUBLISHED.toString()) + } + true + }) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityAdapter.kt new file mode 100644 index 000000000000..a4cc442c316e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityAdapter.kt @@ -0,0 +1,44 @@ +package org.wordpress.android.ui.posts.prepublishing.visibility + +import android.content.Context +import android.view.ViewGroup +import androidx.annotation.MainThread +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView.Adapter +import org.wordpress.android.WordPress +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.VisibilityUiState +import org.wordpress.android.ui.utils.UiHelpers +import javax.inject.Inject + +class PrepublishingVisibilityAdapter(context: Context) : Adapter() { + private var items = mutableListOf() + @Inject lateinit var uiHelpers: UiHelpers + + init { + (context.applicationContext as WordPress).component().inject(this) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PrepublishingVisibilityListItemViewHolder { + return PrepublishingVisibilityListItemViewHolder(parent, uiHelpers) + } + + @MainThread + fun update(newItems: List) { + val diffResult = DiffUtil.calculateDiff( + PrepublishingVisibilityDiffCallback( + items.toList(), + newItems + ) + ) + items.clear() + items.addAll(newItems) + diffResult.dispatchUpdatesTo(this) + } + + override fun getItemCount() = items.size + + override fun onBindViewHolder(holder: PrepublishingVisibilityListItemViewHolder, position: Int) { + val item = items[position] + holder.bind(item) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityDiffCallback.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityDiffCallback.kt new file mode 100644 index 000000000000..867c92e31454 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityDiffCallback.kt @@ -0,0 +1,25 @@ +package org.wordpress.android.ui.posts.prepublishing.visibility + +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.VisibilityUiState +import androidx.recyclerview.widget.DiffUtil.Callback + +class PrepublishingVisibilityDiffCallback( + private val oldList: List, + private val newList: List +) : Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val newItem = newList[newItemPosition] + val oldItem = oldList[oldItemPosition] + + return (oldItem.visibility == newItem.visibility) + } + + override fun getOldListSize(): Int = oldList.size + + override fun getNewListSize(): Int = newList.size + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean = oldList[oldItemPosition] == newList[newItemPosition] +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityFragment.kt new file mode 100644 index 000000000000..be3533dd32d3 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityFragment.kt @@ -0,0 +1,157 @@ +package org.wordpress.android.ui.posts.prepublishing.visibility + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.android.synthetic.main.prepublishing_toolbar.* +import kotlinx.android.synthetic.main.prepublishing_visibility_fragment.* +import org.wordpress.android.R +import org.wordpress.android.R.string +import org.wordpress.android.WordPress +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.EditPostSettingsFragment.EditPostActivityHook +import org.wordpress.android.ui.posts.PostSettingsInputDialogFragment +import org.wordpress.android.ui.posts.PostSettingsInputDialogFragment.PostSettingsInputDialogListener +import org.wordpress.android.ui.posts.PrepublishingScreenClosedListener +import org.wordpress.android.ui.utils.UiHelpers +import javax.inject.Inject + +class PrepublishingVisibilityFragment : Fragment(), PostSettingsInputDialogListener { + @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject lateinit var uiHelpers: UiHelpers + + private lateinit var viewModel: PrepublishingVisibilityViewModel + private var closeListener: PrepublishingScreenClosedListener? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireNotNull(activity).application as WordPress).component().inject(this) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + closeListener = parentFragment as PrepublishingScreenClosedListener + } + + override fun onDetach() { + super.onDetach() + closeListener = null + } + + override fun onResume() { + super.onResume() + reattachPostPasswordDialogListener() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.prepublishing_visibility_fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initRecyclerView() + initViewModel() + initViews() + } + + private fun initRecyclerView() { + visibility_recycler_view.layoutManager = LinearLayoutManager(requireActivity()) + visibility_recycler_view.adapter = PrepublishingVisibilityAdapter(requireActivity()) + } + + private fun initViewModel() { + viewModel = ViewModelProviders.of(this, viewModelFactory) + .get(PrepublishingVisibilityViewModel::class.java) + + viewModel.uiState.observe(viewLifecycleOwner, Observer { uiState -> + (visibility_recycler_view.adapter as PrepublishingVisibilityAdapter).update(uiState) + }) + + viewModel.showPasswordDialog.observe(viewLifecycleOwner, Observer { event -> + event?.applyIfNotHandled { + showPostPasswordDialog() + } + }) + + viewModel.dismissBottomSheet.observe(viewLifecycleOwner, Observer { event -> + event?.applyIfNotHandled { + closeListener?.onCloseClicked() + } + }) + + viewModel.navigateToHomeScreen.observe(viewLifecycleOwner, Observer { event -> + event?.applyIfNotHandled { + closeListener?.onBackClicked() + } + }) + + viewModel.toolbarUiState.observe(viewLifecycleOwner, Observer { uiString -> + toolbar_title.text = uiHelpers.getTextOfUiString( + requireContext(), + uiString + ) + }) + + viewModel.start(getEditPostRepository()) + } + + private fun initViews() { + close_button.setOnClickListener { viewModel.onCloseButtonClicked() } + back_button.setOnClickListener { viewModel.onBackButtonClicked() } + } + + private fun getEditPostRepository(): EditPostRepository { + val editPostActivityHook = requireNotNull(getEditPostActivityHook()) { + "This is possibly null because it's " + + "called during config changes." + } + + return editPostActivityHook.editPostRepository + } + + private fun showPostPasswordDialog() { + val dialog = PostSettingsInputDialogFragment.newInstance( + getEditPostRepository().password, getString(string.password), + getString(string.post_settings_password_dialog_hint), false + ) + dialog.setPostSettingsInputDialogListener(this) + + fragmentManager?.let { + dialog.show(it, PostSettingsInputDialogFragment.TAG) + } + } + + private fun reattachPostPasswordDialogListener() { + val fragment = fragmentManager?.findFragmentByTag(PostSettingsInputDialogFragment.TAG) + fragment?.let { + (it as PostSettingsInputDialogFragment).setPostSettingsInputDialogListener(this) + } + } + + override fun onInputUpdated(input: String) { + viewModel.onPostPasswordChanged(input) + } + + private fun getEditPostActivityHook(): EditPostActivityHook? { + val activity = activity ?: return null + return if (activity is EditPostActivityHook) { + activity + } else { + throw RuntimeException("$activity must implement EditPostActivityHook") + } + } + + companion object { + const val TAG = "prepublishing_visibility_fragment_tag" + fun newInstance() = PrepublishingVisibilityFragment() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityListItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityListItemViewHolder.kt new file mode 100644 index 000000000000..df7203ea9181 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityListItemViewHolder.kt @@ -0,0 +1,37 @@ +package org.wordpress.android.ui.posts.prepublishing.visibility + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.RadioButton +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import org.wordpress.android.R +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.VisibilityUiState +import org.wordpress.android.ui.utils.UiHelpers + +class PrepublishingVisibilityListItemViewHolder(internal val parent: ViewGroup, val uiHelpers: UiHelpers) : ViewHolder( + LayoutInflater.from(parent.context).inflate( + R.layout.prepublishing_visibility_list_item, parent, false + ) +) { + private val visibilityLayout: View = itemView.findViewById(R.id.visibility_layout) + private val visibilityRadioButton: RadioButton = itemView.findViewById(R.id.visibility_radio_button) + private val visibilityText: TextView = itemView.findViewById(R.id.visibility_text) + + init { + visibilityRadioButton.buttonTintList = ContextCompat.getColorStateList( + parent.context, + R.color.neutral_10_primary_40_selector + ) + } + + fun bind(uiState: VisibilityUiState) { + visibilityRadioButton.isChecked = uiState.checked + uiHelpers.setTextOrHide(visibilityText, uiState.visibility.textRes) + visibilityLayout.setOnClickListener { + uiState.onItemTapped.invoke(uiState.visibility) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityViewModel.kt new file mode 100644 index 000000000000..f5a029fbad6c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/PrepublishingVisibilityViewModel.kt @@ -0,0 +1,142 @@ +package org.wordpress.android.ui.posts.prepublishing.visibility + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.DRAFT +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PASSWORD_PROTECTED +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PENDING_REVIEW +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PRIVATE +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PUBLISH +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.VisibilityUiState +import org.wordpress.android.ui.posts.prepublishing.visibility.usecases.GetPostVisibilityUseCase +import org.wordpress.android.ui.posts.prepublishing.visibility.usecases.UpdatePostPasswordUseCase +import org.wordpress.android.ui.posts.prepublishing.visibility.usecases.UpdateVisibilityUseCase +import org.wordpress.android.ui.posts.trackPrepublishingNudges +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.viewmodel.Event +import javax.inject.Inject + +class PrepublishingVisibilityViewModel @Inject constructor( + private val getPostVisibilityUseCase: GetPostVisibilityUseCase, + private val updatePostPasswordUseCase: UpdatePostPasswordUseCase, + private val updatePostStatusUseCase: UpdateVisibilityUseCase, + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper +) : ViewModel() { + private var isStarted = false + private lateinit var editPostRepository: EditPostRepository + + private val _uiState = MutableLiveData>() + val uiState: LiveData> = _uiState + + private val _showPasswordDialog = MutableLiveData>() + val showPasswordDialog: LiveData> = _showPasswordDialog + + private val _navigateToHomeScreen = MutableLiveData>() + val navigateToHomeScreen: LiveData> = _navigateToHomeScreen + + private val _dismissBottomSheet = MutableLiveData>() + val dismissBottomSheet: LiveData> = _dismissBottomSheet + + private val _toolbarUiState = MutableLiveData() + val toolbarUiState: LiveData = _toolbarUiState + + fun start(editPostRepository: EditPostRepository) { + this.editPostRepository = editPostRepository + if (isStarted) return + isStarted = true + + setToolbarUiState() + updateUiState() + } + + private fun setToolbarUiState() { + _toolbarUiState.postValue(UiStringRes(R.string.prepublishing_nudges_toolbar_title_visibility)) + } + + private fun updateUiState() { + val currentVisibility = getPostVisibilityUseCase.getVisibility(editPostRepository) + val items = listOf( + VisibilityUiState( + visibility = PUBLISH, + checked = currentVisibility == PUBLISH, + onItemTapped = ::onVisibilityItemTapped + ), + VisibilityUiState( + visibility = DRAFT, + checked = currentVisibility == DRAFT, + onItemTapped = ::onVisibilityItemTapped + ), + VisibilityUiState( + visibility = PENDING_REVIEW, + checked = currentVisibility == PENDING_REVIEW, + onItemTapped = ::onVisibilityItemTapped + ), + VisibilityUiState( + visibility = PRIVATE, + checked = currentVisibility == PRIVATE, + onItemTapped = ::onVisibilityItemTapped + ), + VisibilityUiState( + visibility = PASSWORD_PROTECTED, + checked = currentVisibility == PASSWORD_PROTECTED, + onItemTapped = ::onVisibilityItemTapped + ) + ) + + _uiState.postValue(items) + } + + private fun onVisibilityItemTapped(visibility: Visibility) { + when { + visibility == PASSWORD_PROTECTED -> _showPasswordDialog.postValue(Event(Unit)) + + editPostRepository.password.isNotEmpty() -> { + // clears the current password so that the PostStatus can be updated and getPostVisibilityUseCase + // will utilize the PostStatus to determine the visibility when the uiState is being updated. + val emptyPassword = "" + updatePostPasswordUseCase.updatePassword(emptyPassword, editPostRepository) { + updatePostStatus(visibility) + } + } + + else -> updatePostStatus(visibility) + } + } + + private fun updatePostStatus(visibility: Visibility) { + analyticsTrackerWrapper.trackPrepublishingNudges(Stat.EDITOR_POST_VISIBILITY_CHANGED) + updatePostStatusUseCase.updatePostVisibility(visibility, editPostRepository, ::updateUiState) + } + + fun onPostPasswordChanged(password: String) { + analyticsTrackerWrapper.trackPrepublishingNudges(Stat.EDITOR_POST_PASSWORD_CHANGED) + updatePostPasswordUseCase.updatePassword(password, editPostRepository, ::updateUiState) + } + + fun onCloseButtonClicked() = _dismissBottomSheet.postValue(Event(Unit)) + + fun onBackButtonClicked() = _navigateToHomeScreen.postValue(Event(Unit)) +} + +sealed class PrepublishingVisibilityItemUiState { + data class VisibilityUiState( + val visibility: Visibility, + val checked: Boolean, + val onItemTapped: ((Visibility) -> Unit) + ) : PrepublishingVisibilityItemUiState() + + enum class Visibility(val textRes: UiStringRes) { + PUBLISH(UiStringRes(R.string.post_status_publish_post)), + DRAFT(UiStringRes(R.string.post_status_draft)), + PENDING_REVIEW(UiStringRes(R.string.post_status_pending_review)), + PRIVATE(UiStringRes(R.string.post_status_post_private)), + PASSWORD_PROTECTED(UiStringRes(R.string.prepublishing_nudges_visibility_password)) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/GetPostVisibilityUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/GetPostVisibilityUseCase.kt new file mode 100644 index 000000000000..083312535b15 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/GetPostVisibilityUseCase.kt @@ -0,0 +1,27 @@ +package org.wordpress.android.ui.posts.prepublishing.visibility.usecases + +import org.wordpress.android.fluxc.model.post.PostStatus +import org.wordpress.android.fluxc.model.post.PostStatus.DRAFT +import org.wordpress.android.fluxc.model.post.PostStatus.PENDING +import org.wordpress.android.fluxc.model.post.PostStatus.PUBLISHED +import org.wordpress.android.fluxc.model.post.PostStatus.SCHEDULED +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PASSWORD_PROTECTED +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PENDING_REVIEW +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PUBLISH +import javax.inject.Inject + +class GetPostVisibilityUseCase @Inject constructor() { + fun getVisibility(editPostRepository: EditPostRepository) = when { + editPostRepository.password.isNotEmpty() -> PASSWORD_PROTECTED + editPostRepository.status == PUBLISHED -> PUBLISH + editPostRepository.status == SCHEDULED -> PUBLISH + editPostRepository.status == PENDING -> PENDING_REVIEW + editPostRepository.status == DRAFT -> Visibility.DRAFT + editPostRepository.status == PostStatus.PRIVATE -> Visibility.PRIVATE + else -> { + throw IllegalStateException("${editPostRepository.status} wasn't resolved by any case in this when clause.") + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/UpdatePostPasswordUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/UpdatePostPasswordUseCase.kt new file mode 100644 index 000000000000..d2be9cbb11e6 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/UpdatePostPasswordUseCase.kt @@ -0,0 +1,19 @@ +package org.wordpress.android.ui.posts.prepublishing.visibility.usecases + +import org.wordpress.android.fluxc.model.PostModel +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.EditPostRepository.UpdatePostResult +import javax.inject.Inject + +class UpdatePostPasswordUseCase @Inject constructor() { + fun updatePassword(password: String, editPostRepository: EditPostRepository, onPostPasswordUpdated: () -> Unit) { + editPostRepository.updateAsync({ postModel: PostModel -> + postModel.setPassword(password) + true + }, { _, result -> + if (result == UpdatePostResult.Updated) { + onPostPasswordUpdated.invoke() + } + }) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/UpdatePostStatusUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/UpdatePostStatusUseCase.kt new file mode 100644 index 000000000000..be471ed18756 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/UpdatePostStatusUseCase.kt @@ -0,0 +1,37 @@ +package org.wordpress.android.ui.posts.prepublishing.visibility.usecases + +import org.wordpress.android.fluxc.model.PostImmutableModel +import org.wordpress.android.fluxc.model.PostModel +import org.wordpress.android.fluxc.model.post.PostStatus +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.EditPostRepository.UpdatePostResult +import org.wordpress.android.ui.posts.PostUtilsWrapper +import org.wordpress.android.util.DateTimeUtilsWrapper +import javax.inject.Inject + +class UpdatePostStatusUseCase @Inject constructor( + private val dateTimeUtilsWrapper: DateTimeUtilsWrapper, + private val postUtilsWrapper: PostUtilsWrapper +) { + fun updatePostStatus( + postStatus: PostStatus, + editPostRepository: EditPostRepository, + onPostStatusUpdated: (PostImmutableModel) -> Unit + ) { + editPostRepository.updateAsync({ postModel: PostModel -> + // we set the date to immediately if it's scheduled. + if (postStatus == PostStatus.PRIVATE) { + if (postUtilsWrapper.isPublishDateInTheFuture(postModel.dateCreated)) + postModel.setDateCreated(dateTimeUtilsWrapper.currentTimeInIso8601()) + } + + postModel.setStatus(postStatus.toString()) + + true + }, { postModel, result -> + if (result == UpdatePostResult.Updated) { + onPostStatusUpdated.invoke(postModel) + } + }) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/UpdateVisibilityUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/UpdateVisibilityUseCase.kt new file mode 100644 index 000000000000..c2cfc68ecd61 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/visibility/usecases/UpdateVisibilityUseCase.kt @@ -0,0 +1,33 @@ +package org.wordpress.android.ui.posts.prepublishing.visibility.usecases + +import org.wordpress.android.fluxc.model.post.PostStatus +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.DRAFT +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PASSWORD_PROTECTED +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PENDING_REVIEW +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PRIVATE +import org.wordpress.android.ui.posts.prepublishing.visibility.PrepublishingVisibilityItemUiState.Visibility.PUBLISH +import javax.inject.Inject + +class UpdateVisibilityUseCase @Inject constructor(private val updatePostStatusUseCase: UpdatePostStatusUseCase) { + fun updatePostVisibility( + visibility: Visibility, + editPostRepository: EditPostRepository, + onPostStatusUpdated: () -> Unit + ) { + val postStatus = when (visibility) { + PUBLISH -> PostStatus.PUBLISHED + DRAFT -> PostStatus.DRAFT + PENDING_REVIEW -> PostStatus.PENDING + PRIVATE -> PostStatus.PRIVATE + PASSWORD_PROTECTED -> { + throw IllegalStateException("$visibility shouldn't be persisted. It does not map to a PostStatus.") + } + } + + updatePostStatusUseCase.updatePostStatus(postStatus, editPostRepository) { + onPostStatusUpdated.invoke() + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/services/AztecVideoLoader.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/AztecVideoLoader.java index 9bab00bf9b8f..9e63eb879742 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/services/AztecVideoLoader.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/services/AztecVideoLoader.java @@ -10,16 +10,22 @@ import android.text.TextUtils; import android.util.DisplayMetrics; +import org.wordpress.android.WordPress; +import org.wordpress.android.ui.utils.AuthenticationUtils; import org.wordpress.android.util.ImageUtils; import org.wordpress.aztec.Html; import java.io.File; +import javax.inject.Inject; + public class AztecVideoLoader implements Html.VideoThumbnailGetter { private Context mContext; private final Drawable mLoadingInProgress; + @Inject AuthenticationUtils mAuthenticationUtils; public AztecVideoLoader(Context context, Drawable loadingInProgressDrawable) { + ((WordPress) WordPress.getContext().getApplicationContext()).component().inject(this); this.mContext = context; this.mLoadingInProgress = loadingInProgressDrawable; } @@ -45,7 +51,7 @@ protected Bitmap doInBackground(Void... params) { return ThumbnailUtils.createVideoThumbnail(url, MediaStore.Images.Thumbnails.FULL_SCREEN_KIND); } - return ImageUtils.getVideoFrameFromVideo(url, maxWidth); + return ImageUtils.getVideoFrameFromVideo(url, maxWidth, mAuthenticationUtils.getAuthHeaders(url)); } protected void onPostExecute(Bitmap thumb) { 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 4324d7141563..bfe7ae8ffdce 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 @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; +import org.wordpress.android.BuildConfig; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; import org.wordpress.android.analytics.AnalyticsTracker.Stat; @@ -223,6 +224,9 @@ public enum UndeletablePrefKey implements PrefKey { // last app version code feature announcement was shown for LAST_FEATURE_ANNOUNCEMENT_APP_VERSION_CODE, + + // feature flag for Reader Improvements Phase 2 + FF_READER_IMPROVEMENTS_PHASE_2 } private static SharedPreferences prefs() { @@ -1158,4 +1162,17 @@ public static boolean isPostWithHWAccelerationOff(int localSiteId, int localPost } return false; } + + /** + * Feature Flag for Reader Improvements Phase 2. Both BuildTime and RunTime feature flag is used. + * + * BuildTime feature flag is used to make sure we never enable the feature in production builds - even when the + * user manually overrides the shared preferences record using adb. + * + * RunTime feature flag is used for us to enable the feature during development. + */ + public static boolean isReaderImprovementsPhase2Enabled() { + return BuildConfig.READER_IMPROVEMENTS_PHASE_2 && getBoolean(UndeletablePrefKey.FF_READER_IMPROVEMENTS_PHASE_2, + false); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java index 07fcf3495c44..39182bd4dea8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java @@ -38,7 +38,6 @@ import org.wordpress.android.ui.whatsnew.FeatureAnnouncementProvider; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppThemeUtils; -import org.wordpress.android.util.CrashLoggingUtils; import org.wordpress.android.util.LocaleManager; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; @@ -102,9 +101,6 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { mAccountStore, hasUserOptedOut); - - CrashLoggingUtils.stopCrashLogging(); - return true; } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java index 5eb761ee5079..bb976a54ba79 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java @@ -377,6 +377,14 @@ public void onDestroyView() { super.onDestroyView(); } + @Override + public void onDestroy() { + if (mSiteSettings != null) { + mSiteSettings.clear(); + } + super.onDestroy(); + } + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (data != null) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java index 6f2e447ce5b1..593641d4fdb9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java @@ -165,7 +165,7 @@ public interface SiteSettingsListener { */ protected abstract void fetchRemoteData(); - protected final Context mContext; + protected Context mContext; protected final SiteModel mSite; protected final SiteSettingsListener mListener; protected final SiteSettingsModel mSettings; @@ -197,6 +197,11 @@ protected void finalize() throws Throwable { super.finalize(); } + public void clear() { + mDispatcher.unregister(this); + mContext = null; + } + public void saveSettings() { SiteSettingsTable.saveSettings(mSettings); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeButtonPrefsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeButtonPrefsFragment.java index d9aeda7489c3..8dbdf7de2729 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeButtonPrefsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeButtonPrefsFragment.java @@ -97,6 +97,14 @@ public void onCreate(@Nullable Bundle savedInstanceState) { mSiteSettings = SiteSettingsInterface.getInterface(getActivity(), mSite, this); } + @Override + public void onDestroy() { + if (mSiteSettings != null) { + mSiteSettings.clear(); + } + super.onDestroy(); + } + @Override public void onSaveInstanceState(@NotNull Bundle outState) { super.onSaveInstanceState(outState); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt index d2597c164522..f49d2548e227 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt @@ -107,7 +107,6 @@ import org.wordpress.android.util.AniUtils import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T import org.wordpress.android.util.AppLog.T.READER -import org.wordpress.android.util.CrashLoggingUtils import org.wordpress.android.util.DateTimeUtils import org.wordpress.android.util.HtmlUtils import org.wordpress.android.util.NetworkUtils @@ -1167,8 +1166,7 @@ class ReaderPostDetailFragment : Fragment(), val icon: Drawable? = try { ContextCompat.getDrawable(it, R.drawable.ic_notice_48dp) } catch (e: Resources.NotFoundException) { - AppLog.e(READER, e) - CrashLoggingUtils.logException(e, READER, "Drawable not found. See issue #11576") + AppLog.e(READER, "Drawable not found. See issue #11576", e) null } icon?.let { 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 0ecad7c0a785..f8ed34053bb1 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 @@ -930,7 +930,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mActionableEmptyView = rootView.findViewById(R.id.empty_custom_view); mRecyclerView.setLogT(AppLog.T.READER); - mRecyclerView.setCustomEmptyView(mActionableEmptyView); + mRecyclerView.setCustomEmptyView(); mRecyclerView.setFilterListener(new FilteredRecyclerView.FilterListener() { @Override public List onLoadFilterCriteriaOptions(boolean refresh) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverFragment.kt index 4ab4afbbc6cc..69c1d59a20a3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverFragment.kt @@ -1,3 +1,29 @@ package org.wordpress.android.ui.reader.discover -class ReaderDiscoverFragment +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import org.wordpress.android.R +import org.wordpress.android.WordPress +import javax.inject.Inject + +class ReaderDiscoverFragment : Fragment(R.layout.reader_discover_fragment_layout) { + @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var viewModel: ReaderDiscoverViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initViewModel() + } + + private fun initViewModel() { + viewModel = ViewModelProviders.of(this, viewModelFactory).get(ReaderDiscoverViewModel::class.java) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt index b2d1fcac658d..c2f1647d4e7e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/ReaderDiscoverViewModel.kt @@ -1,3 +1,6 @@ package org.wordpress.android.ui.reader.discover -class ReaderDiscoverViewModel +import androidx.lifecycle.ViewModel +import javax.inject.Inject + +class ReaderDiscoverViewModel @Inject constructor() : ViewModel() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsFragment.kt new file mode 100644 index 000000000000..c269ffbe3e6c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsFragment.kt @@ -0,0 +1,122 @@ +package org.wordpress.android.ui.reader.discover.interests + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import com.google.android.material.chip.Chip +import kotlinx.android.synthetic.main.reader_interests_fragment_layout.* +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.ui.reader.ReaderFragment +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel.DoneButtonUiState +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel.InterestUiState +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel.UiState.ContentLoadSuccessUiState +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel.UiState.InitialUiState +import org.wordpress.android.ui.utils.UiHelpers +import javax.inject.Inject + +class ReaderInterestsFragment : Fragment(R.layout.reader_interests_fragment_layout) { + @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var viewModel: ReaderInterestsViewModel + @Inject lateinit var uiHelpers: UiHelpers + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initDoneButton() + initViewModel() + } + + private fun initDoneButton() { + done_button.setOnClickListener { + viewModel.onDoneButtonClick() + } + } + + private fun initViewModel() { + viewModel = ViewModelProviders.of(this, viewModelFactory) + .get(ReaderInterestsViewModel::class.java) + startObserving() + } + + private fun startObserving() { + viewModel.uiState.observe(viewLifecycleOwner, Observer { uiState -> + when (uiState) { + is InitialUiState -> { + updateDoneButton(uiState.doneButtonUiState) + } + is ContentLoadSuccessUiState -> { + updateDoneButton(uiState.doneButtonUiState) + updateInterests(uiState.interestsUiState) + } + } + with(uiHelpers) { + updateVisibility(progress_bar, uiState.progressBarVisible) + updateVisibility(title, uiState.titleVisible) + updateVisibility(subtitle, uiState.subtitleVisible) + } + }) + + viewModel.navigateToDiscover.observe(viewLifecycleOwner, Observer { event -> + event?.getContentIfNotHandled()?.let { + navigateToDiscover() + } + }) + + viewModel.start() + } + + private fun updateDoneButton(doneButtonUiState: DoneButtonUiState) { + with(done_button) { + isEnabled = doneButtonUiState.enabled + text = getString(doneButtonUiState.titleRes) + } + uiHelpers.updateVisibility(done_button, doneButtonUiState.visible) + } + + private fun updateInterests(interestsUiState: List) { + interestsUiState.forEachIndexed { index, interestTagUiState -> + val chip = interests_chip_group.findViewWithTag(interestTagUiState.title) + ?: createChipView(interestTagUiState.title, index) + with(chip) { + text = interestTagUiState.title + isChecked = interestTagUiState.isChecked + } + } + } + + private fun createChipView(titleTag: String, index: Int): Chip { + val chip = layoutInflater.inflate( + R.layout.reader_interest_filter_chip, + interests_chip_group, + false + ) as Chip + with(chip) { + layoutDirection = View.LAYOUT_DIRECTION_LOCALE + tag = titleTag + setOnCheckedChangeListener { compoundButton, isChecked -> + if (compoundButton.isPressed) { + viewModel.onInterestAtIndexToggled(index, isChecked) + } + } + interests_chip_group.addView(chip) + } + return chip + } + + private fun navigateToDiscover() { + val fragmentTransaction = parentFragmentManager.beginTransaction() + fragmentTransaction.setCustomAnimations( + R.anim.activity_slide_in_from_right, R.anim.activity_slide_out_to_left, + R.anim.activity_slide_in_from_left, R.anim.activity_slide_out_to_right + ) + fragmentTransaction.replace(R.id.fragment_container, ReaderFragment(), tag).commit() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModel.kt new file mode 100644 index 000000000000..079e639302a9 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsViewModel.kt @@ -0,0 +1,163 @@ +package org.wordpress.android.ui.reader.discover.interests + +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.wordpress.android.R +import org.wordpress.android.models.ReaderTagList +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel.DoneButtonUiState.DoneButtonDisabledUiState +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel.DoneButtonUiState.DoneButtonEnabledUiState +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel.DoneButtonUiState.DoneButtonHiddenUiState +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel.UiState.InitialUiState +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewModel.UiState.ContentLoadSuccessUiState +import org.wordpress.android.ui.reader.repository.ReaderTagRepository +import org.wordpress.android.viewmodel.Event +import javax.inject.Inject + +class ReaderInterestsViewModel @Inject constructor( + private val readerTagRepository: ReaderTagRepository +) : ViewModel() { + private var isStarted = false + + private val _uiState: MutableLiveData = MutableLiveData(InitialUiState) + val uiState: LiveData = _uiState + + private val _navigateToDiscover = MutableLiveData>() + val navigateToDiscover: LiveData> = _navigateToDiscover + + fun start() { + if (isStarted) { + return + } + loadInterests() + isStarted = true + } + + private fun loadInterests() { + viewModelScope.launch { + val tagList = readerTagRepository.getInterests() + val currentUiState = uiState.value as UiState + updateUiState( + ContentLoadSuccessUiState( + interestTagsUiState = transformToInterestsUiState(tagList), + interestTags = tagList, + doneBtnUiState = currentUiState.getDoneButtonState() + ) + ) + } + } + + fun onInterestAtIndexToggled(index: Int, isChecked: Boolean) { + uiState.value?.let { + val currentUiState = uiState.value as ContentLoadSuccessUiState + val updatedInterestsUiState = getUpdatedInterestsUiState(index, isChecked) + + updateUiState( + currentUiState.copy( + interestTagsUiState = updatedInterestsUiState, + doneBtnUiState = currentUiState.getDoneButtonState(isInterestChecked = isChecked) + ) + ) + } + } + + fun onDoneButtonClick() { + viewModelScope.launch { + val currentUiState = uiState.value as UiState + readerTagRepository.saveInterests(currentUiState.getSelectedInterests()) + _navigateToDiscover.value = Event(Unit) + } + } + + private fun transformToInterestsUiState(interests: ReaderTagList) = + interests.map { interest -> + InterestUiState(interest.tagTitle) + } + + private fun getUpdatedInterestsUiState(index: Int, isChecked: Boolean): List { + val currentUiState = uiState.value as UiState + val newInterestsUiState = currentUiState.interestsUiState.toMutableList() + newInterestsUiState[index] = currentUiState.interestsUiState[index].copy(isChecked = isChecked) + return newInterestsUiState + } + + private fun updateUiState(uiState: UiState) { + _uiState.value = uiState + } + + sealed class UiState( + val interestsUiState: List, + val interests: ReaderTagList, + val doneButtonUiState: DoneButtonUiState, + val progressBarVisible: Boolean = false, + val titleVisible: Boolean = false, + val subtitleVisible: Boolean = false + ) { + object InitialUiState : UiState( + interestsUiState = emptyList(), + interests = ReaderTagList(), + doneButtonUiState = DoneButtonHiddenUiState, + progressBarVisible = true + ) + + data class ContentLoadSuccessUiState( + val interestTagsUiState: List, + val interestTags: ReaderTagList, + val doneBtnUiState: DoneButtonUiState + ) : UiState( + interestsUiState = interestTagsUiState, + interests = interestTags, + doneButtonUiState = doneBtnUiState, + progressBarVisible = false, + titleVisible = true, + subtitleVisible = true + ) + + private val checkedInterestsUiState = interestsUiState.filter { it.isChecked } + + fun getSelectedInterests() = interests.filter { + checkedInterestsUiState.map { + checkedInterestUiState -> checkedInterestUiState.title + }.contains(it.tagTitle) + } + + fun getDoneButtonState( + isInterestChecked: Boolean = false + ): DoneButtonUiState { + val disableDoneButton = interests.isEmpty() || (checkedInterestsUiState.size == 1 && !isInterestChecked) + return if (disableDoneButton) { + DoneButtonDisabledUiState + } else { + DoneButtonEnabledUiState + } + } + } + + data class InterestUiState( + val title: String, + val isChecked: Boolean = false + ) + + sealed class DoneButtonUiState( + @StringRes val titleRes: Int = R.string.reader_btn_done, + val enabled: Boolean = false, + val visible: Boolean = true + ) { + object DoneButtonEnabledUiState : DoneButtonUiState( + titleRes = R.string.reader_btn_done, + enabled = true + ) + + object DoneButtonDisabledUiState : DoneButtonUiState( + titleRes = R.string.reader_btn_select_few_interests, + enabled = false + ) + + object DoneButtonHiddenUiState : DoneButtonUiState( + visible = false + ) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt index 0b24e7033a64..382e8b9eb610 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderPostRepository.kt @@ -1,3 +1,453 @@ package org.wordpress.android.ui.reader.repository -class ReaderPostRepository +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.wordpress.android.models.ReaderPostList +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.modules.IO_THREAD +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Named + +class ReaderPostRepository @Inject constructor( + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, + @Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher +) { + companion object { + const val discoverJson = "{\n" + + " \"found\": 3978,\n" + + " \"posts\": [\n" + + " {\n" + + " \"ID\": 42190,\n" + + " \"site_ID\": 53424024,\n" + + " \"author\": {\n" + + " \"ID\": 118930156,\n" + + " \"login\": \"carolynannewells\",\n" + + " \"email\": false,\n" + + " \"name\": \"Carolyn Wells\",\n" + + " \"first_name\": \"Carolyn\",\n" + + " \"last_name\": \"Wells\",\n" + + " \"nice_name\": \"carolynannewells\",\n" + + " \"URL\": \"http:\\/\\/onceuponatime66.wordpress.com\",\n" + + " \"avatar_URL\": \"https:\\/\\/0.gravatar.com\\/avatar\\/" + + "c5e37740ae83e815e3c4e9ef94367509?s=96&d=identicon&r=G\",\n" + + " \"profile_URL\": \"https:\\/\\/en.gravatar.com\\/carolynannewells\",\n" + + " \"site_ID\": 126167003,\n" + + " \"has_avatar\": true\n" + + " },\n" + + " \"date\": \"2020-06-02T10:00:08-04:00\",\n" + + " \"modified\": \"2020-06-01T19:46:26-04:00\",\n" + + " \"title\": \"Tabitha Farrar on Eating Disorder Recovery\",\n" + + " \"URL\": \"https:\\/\\/discover.wordpress.com\\/2020\\/06\\/02\\/" + + "tabitha-farrar-on-eating-disorder-recovery\\/\",\n" + + " \"short_URL\": \"https:\\/\\/wp.me\\/p3Ca1O-aYu\",\n" + + " \"content\": \"

This is content ... Tabitha Farrar, one of the founders " + + "of World Eating Disorders Action Day, shares her experience of blogging about " + + "recovery — and supporting others in their own journeys.<\\/p>\\n\",\n" + + " \"excerpt\": \"

Tabitha Farrar, one of the founders of World Eating " + + "Disorders Action Day, shares her experience of blogging about recovery — " + + "and supporting others in their own journeys.<\\/p>\\n\",\n" + + " \"slug\": \"tabitha-farrar-on-eating-disorder-recovery\",\n" + + " \"guid\": \"https:\\/\\/discover.wordpress.com\\/?p=42190\",\n" + + " \"status\": \"publish\",\n" + + " \"sticky\": false,\n" + + " \"password\": \"\",\n" + + " \"parent\": false,\n" + + " \"type\": \"post\",\n" + + " \"discussion\": {\n" + + " \"comments_open\": true,\n" + + " \"comment_status\": \"open\",\n" + + " \"pings_open\": false,\n" + + " \"ping_status\": \"closed\",\n" + + " \"comment_count\": 0\n" + + " },\n" + + " \"likes_enabled\": true,\n" + + " \"sharing_enabled\": true,\n" + + " \"like_count\": 30,\n" + + " \"i_like\": false,\n" + + " \"is_reblogged\": false,\n" + + " \"is_following\": false,\n" + + " \"global_ID\": \"e12043ca2dc1d8d291e8a2837c6253d6\",\n" + + " \"featured_image\": \"https:\\/\\/discover.files.wordpress.com\\/2020\\/06\\/" + + "screen-shot-2020-06-01-at-9.29.38-am.png\",\n" + + " \"post_thumbnail\": {\n" + + " \"ID\": 42195,\n" + + " \"URL\": \"https:\\/\\/discover.files.wordpress.com\\/2020\\/06\\/" + + "screen-shot-2020-06-01-at-9.29.38-am.png\",\n" + + " \"guid\": \"http:\\/\\/discover.files.wordpress.com\\/2020\\/06\\/" + + "screen-shot-2020-06-01-at-9.29.38-am.png\",\n" + + " \"mime_type\": \"image\\/png\",\n" + + " \"width\": 1934,\n" + + " \"height\": 1726\n" + + " },\n" + + " \"format\": \"standard\",\n" + + " \"geo\": false,\n" + + " \"menu_order\": 0,\n" + + " \"page_template\": \"\",\n" + + " \"publicize_URLs\": [],\n" + + " \"terms\": {\n" + + " \"category\": {\n" + + " \"Education\": {\n" + + " \"ID\": 1342,\n" + + " \"name\": \"Education\",\n" + + " \"slug\": \"education\",\n" + + " \"description\": \"Resources across disciplines and perspectives on " + + "teaching, learning, and the educational system from educators, teachers, and parents.\",\n" + + " \"post_count\": 162,\n" + + " \"parent\": 0,\n" + + " \"meta\": {\n" + + " \"links\": {\n" + + " \"self\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/" + + "sites\\/53424024\\/categories\\/slug:education\",\n" + + " \"help\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/categories\\/slug:education\\/help\",\n" + + " \"site\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"Writing\": {\n" + + " \"ID\": 349,\n" + + " \"name\": \"Writing\",\n" + + " \"slug\": \"writing\",\n" + + " \"description\": \"Writing, advice, and commentary on the act and " + + "process of writing, blogging, and publishing.\",\n" + + " \"post_count\": 527,\n" + + " \"parent\": 0,\n" + + " \"meta\": {\n" + + " \"links\": {\n" + + " \"self\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/" + + "sites\\/53424024\\/categories\\/slug:writing\",\n" + + " \"help\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/categories\\/slug:writing\\/help\",\n" + + " \"site\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\"\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"post_tag\": {\n" + + " \"#ShareYourStory\": {\n" + + " \"ID\": 83876228,\n" + + " \"name\": \"#ShareYourStory\",\n" + + " \"slug\": \"shareyourstory\",\n" + + " \"description\": \"\",\n" + + " \"post_count\": 1,\n" + + " \"meta\": {\n" + + " \"links\": {\n" + + " \"self\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/" + + "sites\\/53424024\\/tags\\/slug:shareyourstory\",\n" + + " \"help\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/tags\\/slug:shareyourstory\\/help\",\n" + + " \"site\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"World Eating Disorders Action Day\": {\n" + + " \"ID\": 501041846,\n" + + " \"name\": \"World Eating Disorders Action Day\",\n" + + " \"slug\": \"world-eating-disorders-action-day\",\n" + + " \"description\": \"\",\n" + + " \"post_count\": 1,\n" + + " \"meta\": {\n" + + " \"links\": {\n" + + " \"self\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/" + + "sites\\/53424024\\/tags\\/slug:world-eating-disorders-action-day\",\n" + + " \"help\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/tags\\/slug:world-eating-disorders-action-day\\/help\",\n" + + " \"site\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\"\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"post_format\": {},\n" + + " \"mentions\": {}\n" + + " },\n" + + " \"tags\": {\n" + + " \"World Eating Disorders Action Day\": {\n" + + " \"ID\": 501041846,\n" + + " \"name\": \"World Eating Disorders Action Day\",\n" + + " \"slug\": \"world-eating-disorders-action-day\",\n" + + " \"description\": \"\",\n" + + " \"post_count\": 1,\n" + + " \"meta\": {\n" + + " \"links\": {\n" + + " \"self\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/" + + "sites\\/53424024\\/tags\\/slug:world-eating-disorders-action-day\",\n" + + " \"help\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/tags\\/slug:world-eating-disorders-action-day\\/help\",\n" + + " \"site\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\"\n" + + " }\n" + + " },\n" + + " \"display_name\": \"world-eating-disorders-action-day\"\n" + + " }\n" + + " },\n" + + " \"categories\": {\n" + + " \"Writing\": {\n" + + " \"ID\": 349,\n" + + " \"name\": \"Writing\",\n" + + " \"slug\": \"writing\",\n" + + " \"description\": \"Writing, advice, and commentary on the act and " + + "process of writing, blogging, and publishing.\",\n" + + " \"post_count\": 527,\n" + + " \"parent\": 0,\n" + + " \"meta\": {\n" + + " \"links\": {\n" + + " \"self\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/" + + "sites\\/53424024\\/categories\\/slug:writing\",\n" + + " \"help\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/categories\\/slug:writing\\/help\",\n" + + " \"site\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\"\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"attachments\": {\n" + + " \"42195\": {\n" + + " \"ID\": 42195,\n" + + " \"URL\": \"https:\\/\\/discover.files.wordpress.com\\/2020\\/06\\/" + + "screen-shot-2020-06-01-at-9.29.38-am.png\",\n" + + " \"guid\": \"http:\\/\\/discover.files.wordpress.com\\/2020\\/06\\/" + + "screen-shot-2020-06-01-at-9.29.38-am.png\",\n" + + " \"date\": \"2020-06-01T12:40:25-04:00\",\n" + + " \"post_ID\": 42190,\n" + + " \"author_ID\": 118930156,\n" + + " \"file\": \"screen-shot-2020-06-01-at-9.29.38-am.png\",\n" + + " \"mime_type\": \"image\\/png\",\n" + + " \"extension\": \"png\",\n" + + " \"title\": \"Screen Shot 2020-06-01 at 9.29.38 AM\",\n" + + " \"caption\": \"Neural Rewiring for Eating Disorder Recovery book cover," + + " tabithafarrar.com\",\n" + + " \"description\": \"\",\n" + + " \"alt\": \"\",\n" + + " \"thumbnails\": {\n" + + " \"thumbnail\": \"https:\\/\\/discover.files.wordpress.com\\/2020\\/06\\/" + + "screen-shot-2020-06-01-at-9.29.38-am.png?w=150\",\n" + + " \"medium\": \"https:\\/\\/discover.files.wordpress.com\\/2020\\/06\\/" + + "screen-shot-2020-06-01-at-9.29.38-am.png?w=315\",\n" + + " \"large\": \"https:\\/\\/discover.files.wordpress.com\\/2020\\/06\\/" + + "screen-shot-2020-06-01-at-9.29.38-am.png?w=1147\",\n" + + " \"newspack-article-block-landscape-large\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=1200&h=900&crop=1\",\n" + + " \"newspack-article-block-portrait-large\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=900&h=1200&crop=1\",\n" + + " \"newspack-article-block-square-large\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=1200&h=1200&crop=1\",\n" + + " \"newspack-article-block-landscape-medium\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=800&h=600&crop=1\",\n" + + " \"newspack-article-block-portrait-medium\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=600&h=800&crop=1\",\n" + + " \"newspack-article-block-square-medium\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=800&h=800&crop=1\",\n" + + " \"newspack-article-block-landscape-small\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=400&h=300&crop=1\",\n" + + " \"newspack-article-block-portrait-small\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=300&h=400&crop=1\",\n" + + " \"newspack-article-block-square-small\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=400&h=400&crop=1\",\n" + + " \"newspack-article-block-landscape-tiny\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=200&h=150&crop=1\",\n" + + " \"newspack-article-block-portrait-tiny\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=150&h=200&crop=1\",\n" + + " \"newspack-article-block-square-tiny\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=200&h=200&crop=1\",\n" + + " \"newspack-article-block-uncropped\": \"https:\\/\\/discover.files." + + "wordpress.com\\/2020\\/06\\/screen-shot-2020-06-01-at-9.29.38-am.png?w=1200\"\n" + + " },\n" + + " \"height\": 1726,\n" + + " \"width\": 1934,\n" + + " \"exif\": {\n" + + " \"aperture\": \"0\",\n" + + " \"credit\": \"\",\n" + + " \"camera\": \"\",\n" + + " \"caption\": \"\",\n" + + " \"created_timestamp\": \"0\",\n" + + " \"copyright\": \"\",\n" + + " \"focal_length\": \"0\",\n" + + " \"iso\": \"0\",\n" + + " \"shutter_speed\": \"0\",\n" + + " \"title\": \"\",\n" + + " \"orientation\": \"0\",\n" + + " \"keywords\": []\n" + + " },\n" + + " \"meta\": {\n" + + " \"links\": {\n" + + " \"self\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/media\\/42195\",\n" + + " \"help\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/media\\/42195\\/help\",\n" + + " \"site\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\",\n" + + " \"parent\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/" + + "sites\\/53424024\\/posts\\/42190\"\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"attachment_count\": 1,\n" + + " \"metadata\": [\n" + + " {\n" + + " \"id\": \"143898\",\n" + + " \"key\": \"geo_public\",\n" + + " \"value\": \"0\"\n" + + " },\n" + + " {\n" + + " \"id\": \"143903\",\n" + + " \"key\": \"_thumbnail_id\",\n" + + " \"value\": \"42195\"\n" + + " },\n" + + " {\n" + + " \"id\": \"143991\",\n" + + " \"key\": \"_wpas_done_22794201\",\n" + + " \"value\": \"1\"\n" + + " },\n" + + " {\n" + + " \"id\": \"143931\",\n" + + " \"key\": \"_wpas_mess\",\n" + + " \"value\": \"On World Eating Disorders Action Day we are sharing the " + + "story of Tabitha Farrar and her recovery blog. #ShareYourStory\"\n" + + " }\n" + + " ],\n" + + " \"meta\": {\n" + + " \"links\": {\n" + + " \"self\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/read\\/" + + "sites\\/53424024\\/posts\\/42190\",\n" + + " \"help\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/posts\\/42190\\/help\",\n" + + " \"site\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\",\n" + + " \"replies\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/" + + "sites\\/53424024\\/posts\\/42190\\/replies\\/\",\n" + + " \"likes\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/" + + "sites\\/53424024\\/posts\\/42190\\/likes\\/\"\n" + + " },\n" + + " \"data\": {\n" + + " \"site\": {\n" + + " \"ID\": 53424024,\n" + + " \"name\": \"Discover\",\n" + + " \"description\": \"A daily selection of the best content published on" + + " WordPress, collected for you by humans who love to read.\",\n" + + " \"URL\": \"https:\\/\\/discover.wordpress.com\",\n" + + " \"jetpack\": false,\n" + + " \"subscribers_count\": 47606255,\n" + + " \"locale\": false,\n" + + " \"icon\": {\n" + + " \"img\": \"https:\\/\\/secure.gravatar.com\\/blavatar\\/" + + "c9e4e04719c81ca4936a63ea2dce6ace\",\n" + + " \"ico\": \"https:\\/\\/secure.gravatar.com\\/blavatar\\/" + + "c9e4e04719c81ca4936a63ea2dce6ace\"\n" + + " },\n" + + " \"logo\": {\n" + + " \"id\": 0,\n" + + " \"sizes\": [],\n" + + " \"url\": \"\"\n" + + " },\n" + + " \"visible\": null,\n" + + " \"is_private\": false,\n" + + " \"is_coming_soon\": false,\n" + + " \"is_following\": false,\n" + + " \"meta\": {\n" + + " \"links\": {\n" + + " \"self\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\",\n" + + " \"help\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/help\",\n" + + " \"posts\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/posts\\/\",\n" + + " \"comments\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.1\\/" + + "sites\\/53424024\\/comments\\/\",\n" + + " \"xmlrpc\": \"https:\\/\\/discover.wordpress.com\\/xmlrpc.php\"\n" + + " }\n" + + " },\n" + + " \"launch_status\": false,\n" + + " \"site_migration\": null,\n" + + " \"is_fse_active\": false,\n" + + " \"is_fse_eligible\": false,\n" + + " \"is_core_site_editor_enabled\": false\n" + + " },\n" + + " \"likes\": {\n" + + " \"found\": 1,\n" + + " \"i_like\": false,\n" + + " \"site_ID\": 53424024,\n" + + " \"post_ID\": 42190,\n" + + " \"likes\": [\n" + + " {\n" + + " \"ID\": 187315272,\n" + + " \"login\": \"erprashantdeep90\",\n" + + " \"email\": false,\n" + + " \"name\": \"The Incredible Mishra\",\n" + + " \"first_name\": \"The Incredible\",\n" + + " \"last_name\": \"Mishra\",\n" + + " \"nice_name\": \"erprashantdeep90\",\n" + + " \"URL\": \"http:\\/\\/theincrediblemishrawritupsart.wordpress.com\",\n" + + " \"avatar_URL\": \"https:\\/\\/2.gravatar.com\\/avatar\\/" + + "2c587712f1bc9bd1eb425e9fa13c76e2?s=96&d=identicon&r=G\",\n" + + " \"profile_URL\": \"https:\\/\\/en.gravatar.com\\/erprashantdeep90\",\n" + + " \"ip_address\": false,\n" + + " \"site_ID\": 178308566,\n" + + " \"site_visible\": true,\n" + + " \"default_avatar\": true\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + " },\n" + + " \"capabilities\": {\n" + + " \"publish_post\": false,\n" + + " \"delete_post\": false,\n" + + " \"edit_post\": false\n" + + " },\n" + + " \"other_URLs\": {},\n" + + " \"feed_ID\": 41325786,\n" + + " \"feed_URL\": \"http:\\/\\/discover.wordpress.com\",\n" + + " \"pseudo_ID\": \"e12043ca2dc1d8d291e8a2837c6253d6\",\n" + + " \"is_external\": false,\n" + + " \"site_name\": \"Discover\",\n" + + " \"site_URL\": \"https:\\/\\/discover.wordpress.com\",\n" + + " \"site_is_private\": false,\n" + + " \"featured_media\": {\n" + + " \"uri\": \"https:\\/\\/discover.files.wordpress.com\\/2020\\/06\\/" + + "gettyimages-521952628.jpg\",\n" + + " \"width\": 6260,\n" + + " \"height\": 2832,\n" + + " \"type\": \"image\"\n" + + " },\n" + + " \"use_excerpt\": false,\n" + + " \"is_following_conversation\": false\n" + + " }\n" + + " ],\n" + + " \"meta\": {\n" + + " \"links\": {\n" + + " \"counts\": \"https:\\/\\/public-api.wordpress.com\\/rest\\/v1.2\\/" + + "sites\\/53424024\\/post-counts\\/post\"\n" + + " },\n" + + " \"next_page\": \"value=2020-06-02T10%3A00%3A08-04%3A00&id=42190\",\n" + + " \"wpcom\": true\n" + + " }\n" + + "}" + } + + private val mutableDiscoveryFeed = MutableLiveData() + val discoveryFeed: LiveData = mutableDiscoveryFeed + + suspend fun getDiscoveryFeed(): LiveData = + withContext(bgDispatcher) { + delay(TimeUnit.SECONDS.toMillis(5)) + getMockDiscoverFeed() + } + + private suspend fun getMockDiscoverFeed(): LiveData { + return withContext(ioDispatcher) { + mutableDiscoveryFeed.postValue(ReaderPostList.fromJson(JSONObject(discoverJson))) + discoveryFeed + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderTagRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderTagRepository.kt index 8e0b80762789..6e93c3a7fe09 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderTagRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/repository/ReaderTagRepository.kt @@ -2,47 +2,68 @@ package org.wordpress.android.ui.reader.repository import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagList import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.modules.IO_THREAD import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit.SECONDS import javax.inject.Inject +import javax.inject.Named /** * ReaderTagRepository is middleware that encapsulates data related business related data logic * Handle communicate with ReaderServices and Actions */ -class ReaderTagRepository @Inject constructor() { - private val mutableTopics = MutableLiveData>() - val topics: LiveData> = mutableTopics +class ReaderTagRepository @Inject constructor( + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, + @Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher +) { + private val mutableRecommendedInterests = MutableLiveData() + val recommendedInterests: LiveData = mutableRecommendedInterests suspend fun getInterests(): ReaderTagList = - withContext(Dispatchers.IO) { - delay(SECONDS.toMillis(5)) - getMockInterests() - } - - // todo: remove method post implementation - private fun getMockInterests() = - ReaderTagList().apply { - for (c in 'A'..'Z') - (add(ReaderTag( - c.toString(), c.toString(), c.toString(), - "https://public-api.wordpress.com/rest/v1.2/read/tags/$c/posts", - ReaderTagType.DEFAULT - ))) + withContext(ioDispatcher) { + delay(SECONDS.toMillis(5)) + getMockInterests() } // todo: full implementation needed suspend fun saveInterests(tags: List) { - CoroutineScope(Dispatchers.IO).launch { + CoroutineScope(ioDispatcher).launch { delay(TimeUnit.SECONDS.toMillis(5)) } } + + suspend fun getRecommendedInterests(): LiveData = + withContext(bgDispatcher) { + delay(TimeUnit.SECONDS.toMillis(5)) + getMockRecommendedInterests() + } + + private suspend fun getMockRecommendedInterests(): LiveData { + return withContext(ioDispatcher) { + mutableRecommendedInterests.postValue(getMockInterests()) + recommendedInterests + } + } + + // todo: remove method post implementation + private fun getMockInterests() = + ReaderTagList().apply { + for (c in 'A'..'Z') + (add( + ReaderTag( + c.toString(), c.toString(), c.toString(), + "https://public-api.wordpress.com/rest/v1.2/read/tags/$c/posts", + ReaderTagType.DEFAULT + ) + )) + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationBaseFormFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationBaseFormFragment.java index 580a31171848..98f9418cb00f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationBaseFormFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationBaseFormFragment.java @@ -46,17 +46,17 @@ protected ViewGroup createMainView(LayoutInflater inflater, ViewGroup container, @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - ViewGroup rootView = createMainView(inflater, container, savedInstanceState); - setupContent(rootView); - return rootView; + return createMainView(inflater, container, savedInstanceState); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + setupContent((ViewGroup) getView().getRootView()); + Toolbar toolbar = view.findViewById(R.id.toolbar_main); - ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar); + ((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar); ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt index 1d5a65da1044..6fe9820b0544 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt @@ -2,7 +2,6 @@ package org.wordpress.android.ui.sitecreation.domains import android.content.Context import android.os.Bundle -import android.view.View import android.view.ViewGroup import androidx.annotation.LayoutRes import androidx.appcompat.widget.AppCompatButton @@ -12,6 +11,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.site_creation_domains_screen.* import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.ui.accounts.HelpActivity @@ -24,16 +24,9 @@ import javax.inject.Inject class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { private lateinit var nonNullActivity: FragmentActivity - private lateinit var linearLayoutManager: LinearLayoutManager - private lateinit var searchInputWithHeader: SearchInputWithHeader - private lateinit var emptyView: View - private lateinit var recyclerView: RecyclerView - private lateinit var createSiteButtonContainer: View + private var searchInputWithHeader: SearchInputWithHeader? = null private lateinit var viewModel: SiteCreationDomainsViewModel - private lateinit var domainsScreenListener: DomainsScreenListener - private lateinit var helpClickedListener: OnHelpClickedListener - @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory @Inject internal lateinit var uiHelpers: UiHelpers @@ -45,8 +38,6 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { if (context !is OnHelpClickedListener) { throw IllegalStateException("Parent activity must implement OnHelpClickedListener.") } - domainsScreenListener = context - helpClickedListener = context } @LayoutRes @@ -60,12 +51,10 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { rootView = rootView, onClear = { viewModel.onClearTextBtnClicked() } ) - emptyView = rootView.findViewById(R.id.domain_list_empty_view) - createSiteButtonContainer = rootView.findViewById(R.id.create_site_button_container) rootView.findViewById(R.id.create_site_button).setOnClickListener { viewModel.createSiteBtnClicked() } - initRecyclerView(rootView) + initRecyclerView() initViewModel() } @@ -88,20 +77,17 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { super.onViewStateRestored(savedInstanceState) // we need to set the `onTextChanged` after the viewState has been restored otherwise the viewModel.updateQuery // is called when the system sets the restored value to the EditText which results in an unnecessary request - searchInputWithHeader.onTextChanged = { viewModel.updateQuery(it) } + searchInputWithHeader?.onTextChanged = { viewModel.updateQuery(it) } } - private fun initRecyclerView(rootView: ViewGroup) { - recyclerView = rootView.findViewById(R.id.recycler_view) - val layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) - linearLayoutManager = layoutManager - recyclerView.layoutManager = linearLayoutManager + private fun initRecyclerView() { + recycler_view.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) initAdapter() } private fun initAdapter() { val adapter = SiteCreationDomainsAdapter(uiHelpers) - recyclerView.adapter = adapter + recycler_view.adapter = adapter } private fun initViewModel() { @@ -110,30 +96,30 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { viewModel.uiState.observe(this, Observer { uiState -> uiState?.let { - searchInputWithHeader.updateHeader(nonNullActivity, uiState.headerUiState) - searchInputWithHeader.updateSearchInput(nonNullActivity, uiState.searchInputUiState) + searchInputWithHeader?.updateHeader(nonNullActivity, uiState.headerUiState) + searchInputWithHeader?.updateSearchInput(nonNullActivity, uiState.searchInputUiState) updateContentUiState(uiState.contentState) - uiHelpers.updateVisibility(createSiteButtonContainer, uiState.createSiteButtonContainerVisibility) + uiHelpers.updateVisibility(create_site_button_container, uiState.createSiteButtonContainerVisibility) } }) viewModel.clearBtnClicked.observe(this, Observer { - searchInputWithHeader.setInputText("") + searchInputWithHeader?.setInputText("") }) viewModel.createSiteBtnClicked.observe(this, Observer { domain -> - domain?.let { domainsScreenListener.onDomainSelected(domain) } + domain?.let { (requireActivity() as DomainsScreenListener).onDomainSelected(domain) } }) viewModel.onHelpClicked.observe(this, Observer { - helpClickedListener.onHelpClicked(HelpActivity.Origin.SITE_CREATION_DOMAINS) + (requireActivity() as OnHelpClickedListener).onHelpClicked(HelpActivity.Origin.SITE_CREATION_DOMAINS) }) viewModel.start(getSegmentIdFromArguments()) } private fun updateContentUiState(contentState: DomainsUiContentState) { - uiHelpers.updateVisibility(emptyView, contentState.emptyViewVisibility) + uiHelpers.updateVisibility(domain_list_empty_view, contentState.emptyViewVisibility) if (contentState.items.isNotEmpty()) { view?.announceForAccessibility(getString(R.string.suggestions_updated_content_description)) } - (recyclerView.adapter as SiteCreationDomainsAdapter).update(contentState.items) + (recycler_view.adapter as SiteCreationDomainsAdapter).update(contentState.items) } private fun getSegmentIdFromArguments(): Long { @@ -142,6 +128,11 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { } } + override fun onDestroyView() { + super.onDestroyView() + searchInputWithHeader = null + } + companion object { const val TAG = "site_creation_domains_fragment_tag" private const val EXTRA_SEGMENT_ID = "extra_segment_id" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt index 33e833f66fcd..4d6a955a85e8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt @@ -193,7 +193,8 @@ class SiteCreationDomainsViewModel @Inject constructor( searchInputUiState = createSearchInputUiState( showProgress = state is Loading, showClearButton = isNonEmptyUserQuery, - showDivider = state.data.isNotEmpty() + showDivider = state.data.isNotEmpty(), + showKeyboard = true ), contentState = createDomainsUiContentState(query, state), createSiteButtonContainerVisibility = selectedDomain != null @@ -298,13 +299,15 @@ class SiteCreationDomainsViewModel @Inject constructor( private fun createSearchInputUiState( showProgress: Boolean, showClearButton: Boolean, - showDivider: Boolean + showDivider: Boolean, + showKeyboard: Boolean ): SiteCreationSearchInputUiState { return SiteCreationSearchInputUiState( hint = UiStringRes(R.string.new_site_creation_search_domain_input_hint), showProgress = showProgress, showClearButton = showClearButton, - showDivider = showDivider + showDivider = showDivider, + showKeyboard = showKeyboard ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SearchInputWithHeader.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SearchInputWithHeader.kt index 0bf7d36ea5e5..c7c97468a2ec 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SearchInputWithHeader.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SearchInputWithHeader.kt @@ -9,6 +9,7 @@ import android.widget.EditText import android.widget.TextView import org.wordpress.android.R import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.ActivityUtils class SearchInputWithHeader(private val uiHelpers: UiHelpers, rootView: View, onClear: () -> Unit) { private val headerLayout = rootView.findViewById(R.id.header_layout) @@ -61,5 +62,13 @@ class SearchInputWithHeader(private val uiHelpers: UiHelpers, rootView: View, on uiHelpers.updateVisibility(progressBar, uiState.showProgress) uiHelpers.updateVisibility(clearAllLayout, uiState.showClearButton) uiHelpers.updateVisibility(divider, uiState.showDivider) + showKeyboard(uiState.showKeyboard) + } + + private fun showKeyboard(shouldShow: Boolean) { + if (shouldShow) { + searchInput.requestFocus() + ActivityUtils.showKeyboard(searchInput) + } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationSearchInputUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationSearchInputUiState.kt index 91f649ea7d3e..17497cd633f5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationSearchInputUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationSearchInputUiState.kt @@ -6,5 +6,6 @@ data class SiteCreationSearchInputUiState( val hint: UiString, val showProgress: Boolean, val showClearButton: Boolean, - val showDivider: Boolean + val showDivider: Boolean, + val showKeyboard: Boolean ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt index dbdd0a967f4d..6cb88677837e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt @@ -15,16 +15,17 @@ import android.view.View.OnLayoutChangeListener import android.view.ViewGroup import android.view.animation.DecelerateInterpolator import android.webkit.WebView -import android.widget.TextView import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders -import com.facebook.shimmer.ShimmerFrameLayout +import kotlinx.android.synthetic.main.site_creation_error_with_retry.* import kotlinx.android.synthetic.main.site_creation_preview_header_item.* import kotlinx.android.synthetic.main.site_creation_preview_screen_default.* +import kotlinx.android.synthetic.main.site_creation_preview_web_view_container.* +import kotlinx.android.synthetic.main.site_creation_progress_creating_site.* import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.ui.accounts.HelpActivity @@ -60,24 +61,11 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), private lateinit var viewModel: SitePreviewViewModel - private lateinit var fullscreenErrorLayout: ViewGroup - private lateinit var fullscreenProgressLayout: ViewGroup - private lateinit var contentLayout: ViewGroup - private lateinit var sitePreviewWebView: WebView - private lateinit var sitePreviewWebError: ViewGroup - private lateinit var sitePreviewWebViewShimmerLayout: ShimmerFrameLayout - private lateinit var sitePreviewWebUrlTitle: TextView - private lateinit var loadingTextLayout: ViewGroup - private lateinit var loadingTextView: TextView + private var animatorSet: AnimatorSet? = null @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory @Inject internal lateinit var uiHelpers: UiHelpers - private lateinit var sitePreviewScreenListener: SitePreviewScreenListener - private lateinit var helpClickedListener: OnHelpClickedListener - - private var okButtonContainer: View? = null - override fun onAttach(context: Context) { super.onAttach(context) if (context !is SitePreviewScreenListener) { @@ -86,8 +74,6 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), if (context !is OnHelpClickedListener) { throw IllegalStateException("Parent activity must implement OnHelpClickedListener.") } - sitePreviewScreenListener = context - helpClickedListener = context } override fun onResume() { @@ -106,16 +92,6 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), } override fun setupContent(rootView: ViewGroup) { - fullscreenErrorLayout = rootView.findViewById(R.id.error_layout) - fullscreenProgressLayout = rootView.findViewById(R.id.progress_layout) - contentLayout = rootView.findViewById(R.id.content_layout) - sitePreviewWebView = rootView.findViewById(R.id.sitePreviewWebView) - sitePreviewWebError = rootView.findViewById(R.id.sitePreviewWebError) - sitePreviewWebViewShimmerLayout = rootView.findViewById(R.id.sitePreviewWebViewShimmerLayout) - sitePreviewWebUrlTitle = rootView.findViewById(R.id.sitePreviewWebUrlTitle) - okButtonContainer = rootView.findViewById(R.id.sitePreviewOkButtonContainer) - loadingTextView = fullscreenProgressLayout.findViewById(R.id.progress_text) - loadingTextLayout = fullscreenProgressLayout.findViewById(R.id.progress_text_layout) initViewModel() initRetryButton() initOkButton() @@ -135,12 +111,12 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), is SitePreviewFullscreenProgressUiState -> updateLoadingLayout(uiState) is SitePreviewFullscreenErrorUiState -> updateErrorLayout(uiState) } - uiHelpers.updateVisibility(fullscreenProgressLayout, uiState.fullscreenProgressLayoutVisibility) - uiHelpers.updateVisibility(contentLayout, uiState.contentLayoutVisibility) + uiHelpers.updateVisibility(progress_layout, uiState.fullscreenProgressLayoutVisibility) + uiHelpers.updateVisibility(content_layout, uiState.contentLayoutVisibility) uiHelpers.updateVisibility(sitePreviewWebView, uiState.webViewVisibility) uiHelpers.updateVisibility(sitePreviewWebError, uiState.webViewErrorVisibility) uiHelpers.updateVisibility(sitePreviewWebViewShimmerLayout, uiState.shimmerVisibility) - uiHelpers.updateVisibility(fullscreenErrorLayout, uiState.fullscreenErrorLayoutVisibility) + uiHelpers.updateVisibility(error_layout, uiState.fullscreenErrorLayoutVisibility) } }) viewModel.preloadPreview.observe(this, Observer { url -> @@ -162,19 +138,19 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), } }) viewModel.onHelpClicked.observe(this, Observer { - helpClickedListener.onHelpClicked(HelpActivity.Origin.SITE_CREATION_CREATING) + (requireActivity() as OnHelpClickedListener).onHelpClicked(HelpActivity.Origin.SITE_CREATION_CREATING) }) viewModel.onSiteCreationCompleted.observe(this, Observer { - sitePreviewScreenListener.onSiteCreationCompleted() + (requireActivity() as SitePreviewScreenListener).onSiteCreationCompleted() }) viewModel.onOkButtonClicked.observe(this, Observer { createSiteState -> createSiteState?.let { - sitePreviewScreenListener.onSitePreviewScreenDismissed(createSiteState) + (requireActivity() as SitePreviewScreenListener).onSitePreviewScreenDismissed(createSiteState) } }) viewModel.onCancelWizardClicked.observe(this, Observer { createSiteState -> createSiteState?.let { - sitePreviewScreenListener.onSitePreviewScreenDismissed(createSiteState) + (requireActivity() as SitePreviewScreenListener).onSitePreviewScreenDismissed(createSiteState) } }) @@ -182,23 +158,19 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), } private fun initRetryButton() { - val retryBtn = fullscreenErrorLayout.findViewById(R.id.error_retry) - retryBtn.setOnClickListener { viewModel.retry() } + error_retry.setOnClickListener { viewModel.retry() } } private fun initContactSupportButton() { - val contactSupport = fullscreenErrorLayout.findViewById(R.id.contact_support) - contactSupport.setOnClickListener { viewModel.onHelpClicked() } + contact_support.setOnClickListener { viewModel.onHelpClicked() } } private fun initCancelWizardButton() { - val cancelBtn = fullscreenErrorLayout.findViewById(R.id.cancel_wizard_button) - cancelBtn.setOnClickListener { viewModel.onCancelWizardClicked() } + cancel_wizard_button.setOnClickListener { viewModel.onCancelWizardClicked() } } private fun initOkButton() { - val okBtn = contentLayout.findViewById(R.id.okButton) - okBtn.setOnClickListener { viewModel.onOkButtonClicked() } + okButton.setOnClickListener { viewModel.onOkButtonClicked() } } private fun updateContentLayout(sitePreviewData: SitePreviewData) { @@ -211,7 +183,7 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), ) } // The view is about to become visible - if (contentLayout.visibility == View.GONE) { + if (content_layout.visibility == View.GONE) { animateContentTransition() view?.announceForAccessibility( getString(R.string.new_site_creation_preview_title) + @@ -222,31 +194,36 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), private fun updateLoadingLayout(progressUiState: SitePreviewFullscreenProgressUiState) { progressUiState.apply { - val newText = uiHelpers.getTextOfUiString(loadingTextView.context, loadingTextResId) + val newText = uiHelpers.getTextOfUiString(progress_text.context, loadingTextResId) AppLog.d(AppLog.T.MAIN, "Changing text - animation: $animate") if (animate) { updateLoadingTextWithFadeAnimation(newText) } else { - loadingTextView.text = newText + progress_text.text = newText } } } private fun updateLoadingTextWithFadeAnimation(newText: String) { val animationDuration = AniUtils.Duration.SHORT - val fadeOut = AniUtils.getFadeOutAnim(loadingTextLayout, animationDuration, View.VISIBLE) - val fadeIn = AniUtils.getFadeInAnim(loadingTextLayout, animationDuration) + val fadeOut = AniUtils.getFadeOutAnim(progress_text_layout, animationDuration, View.VISIBLE) + val fadeIn = AniUtils.getFadeInAnim(progress_text_layout, animationDuration) // update the text when the view isn't visible fadeIn.addListener(object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator) { - loadingTextView.text = newText + progress_text.text = newText + } + + override fun onAnimationEnd(animation: Animator?) { + super.onAnimationEnd(animation) + animatorSet = null } }) // Start the fadein animation right after the view fades out - fadeIn.startDelay = animationDuration.toMillis(loadingTextLayout.context) + fadeIn.startDelay = animationDuration.toMillis(progress_text_layout.context) - AnimatorSet().apply { + animatorSet = AnimatorSet().apply { playSequentially(fadeOut, fadeIn) start() } @@ -254,16 +231,10 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), private fun updateErrorLayout(errorUiStateState: SitePreviewFullscreenErrorUiState) { errorUiStateState.apply { - uiHelpers.setTextOrHide(fullscreenErrorLayout.findViewById(R.id.error_title), titleResId) - uiHelpers.setTextOrHide(fullscreenErrorLayout.findViewById(R.id.error_subtitle), subtitleResId) - uiHelpers.updateVisibility( - fullscreenErrorLayout.findViewById(R.id.contact_support), - errorUiStateState.showContactSupport - ) - uiHelpers.updateVisibility( - fullscreenErrorLayout.findViewById(R.id.cancel_wizard_button), - errorUiStateState.showCancelWizardButton - ) + uiHelpers.setTextOrHide(error_title, titleResId) + uiHelpers.setTextOrHide(error_subtitle, subtitleResId) + uiHelpers.updateVisibility(contact_support, errorUiStateState.showContactSupport) + uiHelpers.updateVisibility(cancel_wizard_button, errorUiStateState.showCancelWizardButton) } } @@ -277,6 +248,13 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), } } + override fun onStop() { + super.onStop() + if (animatorSet?.isRunning == true) { + animatorSet?.cancel() + } + } + /** * Creates a spannable url with 2 different text colors for the subdomain and domain. * @@ -346,7 +324,7 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), } private fun animateContentTransition() { - contentLayout.addOnLayoutChangeListener(object : OnLayoutChangeListener { + content_layout.addOnLayoutChangeListener(object : OnLayoutChangeListener { override fun onLayoutChange( v: View?, left: Int, @@ -358,14 +336,16 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), oldRight: Int, oldBottom: Int ) { - if (contentLayout.measuredWidth > 0 && contentLayout.measuredHeight > 0) { - contentLayout.removeOnLayoutChangeListener(this) - val contentHeight = contentLayout.measuredHeight.toFloat() + if (content_layout.measuredWidth > 0 && content_layout.measuredHeight > 0) { + content_layout.removeOnLayoutChangeListener(this) + val contentHeight = content_layout.measuredHeight.toFloat() val titleAnim = createFadeInAnimator(sitePreviewTitle) val webViewAnim = createSlideInFromBottomAnimator(webviewContainer, contentHeight) // OK button should slide in if the container exists and fade in otherwise - val okAnim = okButtonContainer?.let { createSlideInFromBottomAnimator(it, contentHeight) } + val okAnim = sitePreviewOkButtonContainer?.let { + createSlideInFromBottomAnimator(it, contentHeight) + } ?: createFadeInAnimator(okButton) AnimatorSet().apply { interpolator = DecelerateInterpolator() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/segments/SiteCreationSegmentsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/segments/SiteCreationSegmentsFragment.kt index f4958e87b1c8..8fb09cd9f28f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/segments/SiteCreationSegmentsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/segments/SiteCreationSegmentsFragment.kt @@ -4,14 +4,14 @@ import android.content.Context import android.os.Bundle import android.view.View import android.view.ViewGroup -import android.widget.Button -import android.widget.TextView import androidx.annotation.LayoutRes import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.site_creation_error_with_retry.* +import kotlinx.android.synthetic.main.site_creation_segments_screen.* import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.ui.accounts.HelpActivity @@ -24,21 +24,12 @@ import org.wordpress.android.util.image.ImageManager import javax.inject.Inject class SiteCreationSegmentsFragment : SiteCreationBaseFormFragment() { - private lateinit var linearLayoutManager: LinearLayoutManager - private lateinit var recyclerView: RecyclerView private lateinit var viewModel: SiteCreationSegmentsViewModel - private lateinit var errorLayout: ViewGroup - private lateinit var errorTitle: TextView - private lateinit var errorSubtitle: TextView - @Inject internal lateinit var imageManager: ImageManager @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory @Inject internal lateinit var uiHelpers: UiHelpers - private lateinit var helpClickedListener: OnHelpClickedListener - private lateinit var segmentsScreenListener: SegmentsScreenListener - override fun onAttach(context: Context) { super.onAttach(context) if (context !is OnHelpClickedListener) { @@ -47,8 +38,6 @@ class SiteCreationSegmentsFragment : SiteCreationBaseFormFragment() { if (context !is SegmentsScreenListener) { throw IllegalStateException("Parent activity must implement SegmentsScreenListener.") } - helpClickedListener = context - segmentsScreenListener = context } @LayoutRes @@ -57,29 +46,19 @@ class SiteCreationSegmentsFragment : SiteCreationBaseFormFragment() { } override fun setupContent(rootView: ViewGroup) { - initErrorLayout(rootView) - initRecyclerView(rootView) + initRecyclerView() initViewModel() - initRetryButton(rootView) + initRetryButton() } - private fun initErrorLayout(rootView: ViewGroup) { - errorLayout = rootView.findViewById(R.id.error_layout) - errorTitle = errorLayout.findViewById(R.id.error_title) - errorSubtitle = errorLayout.findViewById(R.id.error_subtitle) - } - - private fun initRecyclerView(rootView: ViewGroup) { - recyclerView = rootView.findViewById(R.id.recycler_view) - val layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) - linearLayoutManager = layoutManager - recyclerView.layoutManager = linearLayoutManager + private fun initRecyclerView() { + recycler_view.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) initAdapter() } private fun initAdapter() { val adapter = SiteCreationSegmentsAdapter(imageManager = imageManager) - recyclerView.adapter = adapter + recycler_view.adapter = adapter } private fun initViewModel() { @@ -90,13 +69,13 @@ class SiteCreationSegmentsFragment : SiteCreationBaseFormFragment() { state?.let { uiState -> when (uiState) { is SegmentsContentUiState -> { - recyclerView.visibility = View.VISIBLE - errorLayout.visibility = View.GONE + recycler_view.visibility = View.VISIBLE + error_layout.visibility = View.GONE updateContentLayout(uiState) } is SegmentsErrorUiState -> { - recyclerView.visibility = View.GONE - errorLayout.visibility = View.VISIBLE + recycler_view.visibility = View.GONE + error_layout.visibility = View.VISIBLE updateErrorLayout(uiState) } } @@ -104,21 +83,24 @@ class SiteCreationSegmentsFragment : SiteCreationBaseFormFragment() { }) viewModel.segmentSelected.observe( this, - Observer { segmentId -> segmentId?.let { segmentsScreenListener.onSegmentSelected(segmentId) } }) + Observer { segmentId -> + segmentId?.let { + (requireActivity() as SegmentsScreenListener).onSegmentSelected(segmentId) + } + }) viewModel.onHelpClicked.observe(this, Observer { - helpClickedListener.onHelpClicked(HelpActivity.Origin.SITE_CREATION_SEGMENTS) + (requireActivity() as OnHelpClickedListener).onHelpClicked(HelpActivity.Origin.SITE_CREATION_SEGMENTS) }) viewModel.start() } private fun updateErrorLayout(errorUiStateState: SegmentsErrorUiState) { - uiHelpers.setTextOrHide(errorTitle, errorUiStateState.titleResId) - uiHelpers.setTextOrHide(errorSubtitle, errorUiStateState.subtitleResId) + uiHelpers.setTextOrHide(error_title, errorUiStateState.titleResId) + uiHelpers.setTextOrHide(error_subtitle, errorUiStateState.subtitleResId) } - private fun initRetryButton(rootView: ViewGroup) { - val retryBtn = rootView.findViewById