diff --git a/.github/workflows/validate-issues.yml b/.github/workflows/validate-issues.yml new file mode 100644 index 000000000000..c9899d62603d --- /dev/null +++ b/.github/workflows/validate-issues.yml @@ -0,0 +1,18 @@ +name: 📝 Validate Issues + +on: + issues: + types: [opened, labeled, unlabeled] + +jobs: + check-labels-on-issues: + uses: Automattic/dangermattic/.github/workflows/reusable-check-labels-on-issues.yml@v1.0.0 + with: + label-format-list: '[ + "^\[.+\]", + "^[[:alnum:]]" + ]' + label-error-message: '🚫 Please add a type label (e.g. **[Type] Enhancement**) and a feature label (e.g. **Stats**) to this issue.' + label-success-message: 'Thanks for reporting! 👍' + secrets: + github-token: ${{ secrets.DANGERMATTIC_GITHUB_TOKEN }} diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index acaabc9039e2..be79913d54c1 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,8 +2,8 @@ 24.3 ----- -- [**] Added support to use third-party passkey providers and other devices passkeys as a WordPress.com login option [https://github.com/wordpress-mobile/WordPress-Android/pull/20174] - +* [**] Added support to use third-party passkey providers and other devices passkeys as a WordPress.com login option [https://github.com/wordpress-mobile/WordPress-Android/pull/20174] +* [*] [Jetpack-only] Fix the visibility issue with the menu button on the stats [https://github.com/wordpress-mobile/WordPress-Android/pull/20175] 24.2 ----- @@ -12,6 +12,7 @@ * [**] Prevent images from temporarily disappearing when uploading media [https://github.com/WordPress/gutenberg/pull/57869] * [***] [Jetpack-only] Reader: introduced new UI/UX for content navigation and filtering [https://github.com/wordpress-mobile/WordPress-Android/pull/19978] * [**] Prevents crashes when the webview state is too big [https://github.com/wordpress-mobile/WordPress-Android/pull/20139] +* [*] [WordPress-only] Prevents a crash occurring when uploading videos under certain conditions [https://github.com/wordpress-mobile/WordPress-Android/pull/20168] 24.1 ----- diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 4b11a35570f3..8e74183734e5 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -342,7 +342,7 @@ kapt { } dependencies { - implementation 'androidx.webkit:webkit:1.7.0' + implementation 'androidx.webkit:webkit:1.10.0' implementation "androidx.navigation:navigation-compose:$androidxComposeNavigationVersion" compileOnly project(path: ':libs:annotations') kapt project(':libs:processors') diff --git a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt index cd4fba1f5bd5..02eb23ed98bc 100644 --- a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt +++ b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt @@ -464,65 +464,78 @@ class AppInitializer @Inject constructor( credentialsClient.connect() } - private fun createNotificationChannelsOnSdk26() { + private fun createNotificationChannelsOnSdk26( + normal: Boolean = true, + important: Boolean = true, + reminder: Boolean = true, + transient: Boolean = true, + weeklyRoundup: Boolean = true + ) { // create Notification channels introduced in Android Oreo if (Build.VERSION.SDK_INT >= VERSION_CODES.O) { - // Create the NORMAL channel (used for likes, comments, replies, etc.) - val normalChannel = NotificationChannel( - application.getString(R.string.notification_channel_normal_id), - application.getString(R.string.notification_channel_general_title), - NotificationManager.IMPORTANCE_DEFAULT - ) - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this val notificationManager = application.getSystemService( Context.NOTIFICATION_SERVICE ) as NotificationManager - notificationManager.createNotificationChannel(normalChannel) + if (normal) { + // Create the NORMAL channel (used for likes, comments, replies, etc.) + val normalChannel = NotificationChannel( + application.getString(R.string.notification_channel_normal_id), + application.getString(R.string.notification_channel_general_title), + NotificationManager.IMPORTANCE_DEFAULT + ) + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this - // Create the IMPORTANT channel (used for 2fa auth, for example) - val importantChannel = NotificationChannel( - application.getString(R.string.notification_channel_important_id), - application.getString(R.string.notification_channel_important_title), - NotificationManager.IMPORTANCE_HIGH - ) - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - notificationManager.createNotificationChannel(importantChannel) - - // Create the REMINDER channel (used for various reminders, like Quick Start, etc.) - val reminderChannel = NotificationChannel( - application.getString(R.string.notification_channel_reminder_id), - application.getString(R.string.notification_channel_reminder_title), - NotificationManager.IMPORTANCE_LOW - ) - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - notificationManager.createNotificationChannel(reminderChannel) - - // Create the TRANSIENT channel (used for short-lived notifications such as processing a Like/Approve, - // or media upload) - val transientChannel = NotificationChannel( - application.getString(R.string.notification_channel_transient_id), - application.getString(R.string.notification_channel_transient_title), - NotificationManager.IMPORTANCE_DEFAULT - ) - transientChannel.setSound(null, null) - transientChannel.enableVibration(false) - transientChannel.enableLights(false) - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - notificationManager.createNotificationChannel(transientChannel) - - // Create the WEEKLY ROUNDUP channel (used for weekly roundup notification containing weekly stats) - val weeklyRoundupChannel = NotificationChannel( - application.getString(R.string.notification_channel_weekly_roundup_id), - application.getString(R.string.notification_channel_weekly_roundup_title), - NotificationManager.IMPORTANCE_LOW - ) - // Register the channel with the system; you can't change the importance or other notification behaviors - // after this - notificationManager.createNotificationChannel(weeklyRoundupChannel) + notificationManager.createNotificationChannel(normalChannel) + } + if (important) { + // Create the IMPORTANT channel (used for 2fa auth, for example) + val importantChannel = NotificationChannel( + application.getString(R.string.notification_channel_important_id), + application.getString(R.string.notification_channel_important_title), + NotificationManager.IMPORTANCE_HIGH + ) + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + notificationManager.createNotificationChannel(importantChannel) + } + if (reminder) { + // Create the REMINDER channel (used for various reminders, like Quick Start, etc.) + val reminderChannel = NotificationChannel( + application.getString(R.string.notification_channel_reminder_id), + application.getString(R.string.notification_channel_reminder_title), + NotificationManager.IMPORTANCE_LOW + ) + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + notificationManager.createNotificationChannel(reminderChannel) + } + if (transient) { + // Create the TRANSIENT channel (used for short-lived notifications such as processing a Like/Approve, + // or media upload) + val transientChannel = NotificationChannel( + application.getString(R.string.notification_channel_transient_id), + application.getString(R.string.notification_channel_transient_title), + NotificationManager.IMPORTANCE_DEFAULT + ) + transientChannel.setSound(null, null) + transientChannel.enableVibration(false) + transientChannel.enableLights(false) + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + notificationManager.createNotificationChannel(transientChannel) + } + if (weeklyRoundup) { + // Create the WEEKLY ROUNDUP channel (used for weekly roundup notification containing weekly stats) + val weeklyRoundupChannel = NotificationChannel( + application.getString(R.string.notification_channel_weekly_roundup_id), + application.getString(R.string.notification_channel_weekly_roundup_title), + NotificationManager.IMPORTANCE_LOW + ) + // Register the channel with the system; you can't change the importance or other notification behaviors + // after this + notificationManager.createNotificationChannel(weeklyRoundupChannel) + } } } @@ -980,10 +993,19 @@ class AppInitializer @Inject constructor( } private fun updateNotificationSettings() { - if(!jetpackFeatureRemovalPhaseHelper.shouldShowNotifications()) + if (!jetpackFeatureRemovalPhaseHelper.shouldShowNotifications()) { NotificationsUtils.cancelAllNotifications(application) - else + // Only create the transient notification channel to handle upload notifications + createNotificationChannelsOnSdk26( + normal = false, + important = false, + reminder = false, + transient = true, + weeklyRoundup = false, + ) + } else { createNotificationChannelsOnSdk26() + } } companion object { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverActivity.java index bbdd6ed3f221..28edf0b25bab 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ShareIntentReceiverActivity.java @@ -4,6 +4,7 @@ import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -21,6 +22,8 @@ import org.wordpress.android.ui.main.WPMainActivity; import org.wordpress.android.ui.media.MediaBrowserActivity; import org.wordpress.android.ui.media.MediaBrowserType; +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; @@ -94,23 +97,33 @@ private void refreshContent() { } private void downloadExternalMedia() { - if (Intent.ACTION_SEND_MULTIPLE.equals(getIntent().getAction())) { - ArrayList externalUris = getIntent().getParcelableArrayListExtra((Intent.EXTRA_STREAM)); - for (Uri uri : externalUris) { - if (uri != null && isAllowedMediaType(uri)) { - mLocalMediaUris.add(MediaUtils.downloadExternalMedia(this, uri)); + try { + if (Intent.ACTION_SEND_MULTIPLE.equals(getIntent().getAction())) { + ArrayList externalUris = getIntent().getParcelableArrayListExtra((Intent.EXTRA_STREAM)); + for (Uri uri : externalUris) { + if (uri != null && isAllowedMediaType(uri)) { + mLocalMediaUris.add(MediaUtils.downloadExternalMedia(this, uri)); + } + } + } else if (Intent.ACTION_SEND.equals(getIntent().getAction())) { + Uri externalUri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); + if (externalUri != null && isAllowedMediaType(externalUri)) { + mLocalMediaUris.add(MediaUtils.downloadExternalMedia(this, externalUri)); } } - } else if (Intent.ACTION_SEND.equals(getIntent().getAction())) { - Uri externalUri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); - if (externalUri != null && isAllowedMediaType(externalUri)) { - mLocalMediaUris.add(MediaUtils.downloadExternalMedia(this, externalUri)); - } + } catch (Exception e) { + ToastUtils.showToast(this, + R.string.error_media_could_not_share_media_from_device, ToastUtils.Duration.LONG); + AppLog.e(T.MEDIA, "ShareIntentReceiver failed to download media ", e); } } private boolean isAllowedMediaType(@NonNull Uri uri) { String filePath = MediaUtils.getRealPathFromURI(this, uri); + // For cases when getRealPathFromURI returns an empty string + if (TextUtils.isEmpty(filePath)) { + filePath = String.valueOf(uri); + } return MediaUtils.isValidImage(filePath) || MediaUtils.isVideo(filePath); } 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 d7bbc9b13c4b..2da410a95ab6 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 @@ -640,7 +640,7 @@ private void initSubFilterViewModel(@Nullable Bundle savedInstanceState) { BottomSheetVisible visibleState = (BottomSheetVisible) uiState; bottomSheet = SubfilterBottomSheetFragment.newInstance( SubFilterViewModel.getViewModelKeyForTag(mTagFragmentStartedWith), - visibleState.getCategories(), + visibleState.getCategory(), mUiHelpers.getTextOfUiString(requireContext(), visibleState.getTitle()) ); bottomSheet.show(getChildFragmentManager(), SUBFILTER_BOTTOM_SHEET_TAG); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java index 9b0d02c08482..955a08936ce5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java @@ -70,7 +70,7 @@ * followed tags and followed blogs */ public class ReaderSubsActivity extends LocaleAwareActivity - implements ReaderTagAdapter.TagDeletedListener { + implements ReaderTagAdapter.TagDeletedListener, ReaderTagAdapter.TagAddedListener { private EditText mEditAdd; private FloatingActionButton mFabButton; private ReaderFollowButton mBtnAdd; @@ -176,8 +176,7 @@ private void setResult() { boolean shouldRefreshSubscriptions = false; if (mPageAdapter != null) { final ReaderTagFragment readerTagFragment = mPageAdapter.getReaderTagFragment(); - final ReaderBlogFragment readerBlogFragment = mPageAdapter.getReaderBlogFragment(); - if (readerTagFragment != null && readerBlogFragment != null) { + if (readerTagFragment != null) { shouldRefreshSubscriptions = readerTagFragment.hasChangedSelectedTags(); } } @@ -497,8 +496,14 @@ public void onTagDeleted(ReaderTag tag) { if (mLastAddedTagName != null && mLastAddedTagName.equalsIgnoreCase(tag.getTagSlug())) { mLastAddedTagName = null; } - String labelRemovedTag = getString(R.string.reader_label_removed_tag); - showInfoSnackbar(String.format(labelRemovedTag, tag.getLabel())); + } + + @Override public void onTagAdded(@NonNull ReaderTag readerTag) { + mReaderTracker.trackTag( + AnalyticsTracker.Stat.READER_TAG_FOLLOWED, + readerTag.getTagSlug(), + ReaderTracker.SOURCE_SETTINGS + ); } /* diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagFragment.java index 1638e7ae1eb0..f767642e9c8f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTagFragment.java @@ -25,7 +25,8 @@ /* * fragment hosted by ReaderSubsActivity which shows followed tags */ -public class ReaderTagFragment extends Fragment implements ReaderTagAdapter.TagDeletedListener { +public class ReaderTagFragment extends Fragment + implements ReaderTagAdapter.TagDeletedListener, ReaderTagAdapter.TagAddedListener { private ReaderRecyclerView mRecyclerView; private ReaderTagAdapter mTagAdapter; @@ -49,10 +50,10 @@ public boolean hasChangedSelectedTags() { for (final ReaderTag readerTag : mInitialReaderTagList) { initialTagsSlugs.add(readerTag.getTagSlug()); } - final List currentReaderTagList = getTagAdapter().getItems(); + final List currentlySubscribedReaderTagList = getTagAdapter().getSubscribedItems(); final Set currentTagsSlugs = new HashSet<>(); - if (currentReaderTagList != null) { - for (final ReaderTag readerTag : currentReaderTagList) { + if (currentlySubscribedReaderTagList != null) { + for (final ReaderTag readerTag : currentlySubscribedReaderTagList) { currentTagsSlugs.add(readerTag.getTagSlug()); } } @@ -103,6 +104,7 @@ private ReaderTagAdapter getTagAdapter() { Context context = WPActivityUtils.getThemedContext(getActivity()); mTagAdapter = new ReaderTagAdapter(context); mTagAdapter.setTagDeletedListener(this); + mTagAdapter.setTagAddedListener(this); mTagAdapter.setDataLoadedListener(isEmpty -> { checkEmptyView(); if (mIsFirstDataLoaded) { @@ -133,4 +135,10 @@ public void onTagDeleted(ReaderTag tag) { ((ReaderTagAdapter.TagDeletedListener) getActivity()).onTagDeleted(tag); } } + + @Override public void onTagAdded(@NonNull ReaderTag readerTag) { + if (getActivity() instanceof ReaderTagAdapter.TagDeletedListener) { + ((ReaderTagAdapter.TagAddedListener) getActivity()).onTagAdded(readerTag); + } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt index 01de1e11aa52..ead55fbc4c12 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt @@ -24,7 +24,7 @@ import org.wordpress.android.ui.reader.subfilter.SubfilterCategory.SITES import org.wordpress.android.ui.reader.subfilter.SubfilterCategory.TAGS import org.wordpress.android.ui.reader.subfilter.SubfilterListItem.Tag import org.wordpress.android.ui.reader.subfilter.SubfilterPagerAdapter -import org.wordpress.android.util.extensions.getParcelableArrayListCompat +import org.wordpress.android.util.extensions.getSerializableCompat import javax.inject.Inject import com.google.android.material.R as MaterialR @@ -36,19 +36,19 @@ class SubfilterBottomSheetFragment : BottomSheetDialogFragment() { companion object { const val SUBFILTER_VIEW_MODEL_KEY = "subfilter_view_model_key" const val SUBFILTER_TITLE_KEY = "subfilter_title_key" - const val SUBFILTER_CATEGORIES_KEY = "subfilter_categories_key" + const val SUBFILTER_CATEGORY_KEY = "subfilter_category_key" @JvmStatic fun newInstance( subfilterViewModelKey: String, - categories: List, + category: SubfilterCategory, title: CharSequence ): SubfilterBottomSheetFragment { val fragment = SubfilterBottomSheetFragment() val bundle = Bundle() bundle.putString(SUBFILTER_VIEW_MODEL_KEY, subfilterViewModelKey) bundle.putCharSequence(SUBFILTER_TITLE_KEY, title) - bundle.putParcelableArrayList(SUBFILTER_CATEGORIES_KEY, ArrayList(categories)) + bundle.putSerializable(SUBFILTER_CATEGORY_KEY, category) fragment.arguments = bundle return fragment @@ -66,11 +66,14 @@ class SubfilterBottomSheetFragment : BottomSheetDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val subfilterVmKey = requireArguments().getString(SUBFILTER_VIEW_MODEL_KEY)!! - val bottomSheetTitle = requireArguments().getCharSequence(SUBFILTER_TITLE_KEY)!! - val categories = requireNotNull( - requireArguments().getParcelableArrayListCompat(SUBFILTER_CATEGORIES_KEY) - ) + val subfilterVmKey = requireArguments().getString(SUBFILTER_VIEW_MODEL_KEY) + val bottomSheetTitle = requireArguments().getCharSequence(SUBFILTER_TITLE_KEY) + val category = requireArguments().getSerializableCompat(SUBFILTER_CATEGORY_KEY) + + if (subfilterVmKey == null || category == null || bottomSheetTitle == null) { + dismiss() + return + } viewModel = ViewModelProvider( parentFragment as ViewModelStoreOwner, @@ -87,7 +90,7 @@ class SubfilterBottomSheetFragment : BottomSheetDialogFragment() { requireActivity(), childFragmentManager, subfilterVmKey, - categories.toList() + listOf(category) ) pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { @@ -110,7 +113,6 @@ class SubfilterBottomSheetFragment : BottomSheetDialogFragment() { } editSubscriptions.setOnClickListener { - val category = categories.firstOrNull() ?: return@setOnClickListener val subsPageIndex = when (category) { SITES -> ReaderSubsActivity.TAB_IDX_FOLLOWED_BLOGS TAGS -> ReaderSubsActivity.TAB_IDX_FOLLOWED_TAGS diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java index 5dc99a307cea..955783b7a630 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java @@ -28,6 +28,8 @@ import org.wordpress.android.util.ToastUtils; import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; import javax.inject.Inject; @@ -36,11 +38,17 @@ public interface TagDeletedListener { void onTagDeleted(ReaderTag tag); } + public interface TagAddedListener { + void onTagAdded(@NonNull ReaderTag readerTag); + } + @Inject AccountStore mAccountStore; private final WeakReference mWeakContext; private final ReaderTagList mTags = new ReaderTagList(); private TagDeletedListener mTagDeletedListener; + private TagAddedListener mTagAddedListener; private ReaderInterfaces.DataLoadedListener mDataLoadedListener; + private final Map mTagSlugIsFollowedMap = new HashMap<>(); public ReaderTagAdapter(Context context) { super(); @@ -53,6 +61,10 @@ public void setTagDeletedListener(TagDeletedListener listener) { mTagDeletedListener = listener; } + public void setTagAddedListener(@NonNull final TagAddedListener listener) { + mTagAddedListener = listener; + } + public void setDataLoadedListener(ReaderInterfaces.DataLoadedListener listener) { mDataLoadedListener = listener; } @@ -99,39 +111,55 @@ public ReaderTagList getItems() { return mTags; } + @Nullable + public ReaderTagList getSubscribedItems() { + final ReaderTagList readerSubscribedTagsList = new ReaderTagList(); + for (final ReaderTag readerTag : mTags) { + if (Boolean.TRUE.equals(mTagSlugIsFollowedMap.get(readerTag.getTagSlug()))) { + readerSubscribedTagsList.add(readerTag); + } + } + return readerSubscribedTagsList; + } + @Override public void onBindViewHolder(TagViewHolder holder, int position) { final ReaderTag tag = mTags.get(position); holder.mTxtTagName.setText(tag.getLabel()); - holder.mRemoveFollowButton.setOnClickListener(v -> performDeleteTag(tag)); + holder.mRemoveFollowButton.setOnClickListener(view -> performDeleteTag(tag, holder.mRemoveFollowButton)); } - private void performDeleteTag(@NonNull ReaderTag tag) { + private void performDeleteTag(@NonNull ReaderTag tag, @NonNull final ReaderFollowButton readerFollowButton) { if (!NetworkUtils.checkConnection(getContext())) { return; } - ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() { - @Override - public void onActionResult(boolean succeeded) { - if (!succeeded && hasContext()) { - ToastUtils.showToast(getContext(), R.string.reader_toast_err_removing_tag); - refresh(); - } - } - }; + final boolean isFollowingCurrent = Boolean.TRUE.equals(mTagSlugIsFollowedMap.get(tag.getTagSlug())); + final boolean isFollowingNew = !isFollowingCurrent; + readerFollowButton.setIsFollowed(isFollowingNew); - boolean success = ReaderTagActions.deleteTag(tag, actionListener, mAccountStore.hasAccessToken()); + // Disable follow button until API call returns + readerFollowButton.setEnabled(false); - if (success) { - int index = mTags.indexOfTagName(tag.getTagSlug()); - if (index > -1) { - mTags.remove(index); - notifyItemRemoved(index); + ReaderActions.ActionListener actionListener = succeeded -> { + mTagSlugIsFollowedMap.put(tag.getTagSlug(), isFollowingNew); + readerFollowButton.setEnabled(true); + if (!succeeded && hasContext()) { + ToastUtils.showToast(getContext(), R.string.reader_toast_err_removing_tag); + refresh(); } - if (mTagDeletedListener != null) { + }; + + if (isFollowingCurrent) { + boolean success = ReaderTagActions.deleteTag(tag, actionListener, mAccountStore.hasAccessToken()); + if (success && mTagDeletedListener != null) { mTagDeletedListener.onTagDeleted(tag); } + } else { + boolean success = ReaderTagActions.addTag(tag, actionListener, mAccountStore.hasAccessToken()); + if (success && mTagAddedListener != null) { + mTagAddedListener.onTagAdded(tag); + } } } @@ -175,6 +203,10 @@ protected void onPostExecute(ReaderTagList tagList) { if (tagList != null && !tagList.isSameList(mTags)) { mTags.clear(); mTags.addAll(tagList); + mTagSlugIsFollowedMap.clear(); + for (final ReaderTag tag : mTags) { + mTagSlugIsFollowedMap.put(tag.getTagSlug(), true); + } notifyDataSetChanged(); } mIsTaskRunning = false; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/BottomSheetUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/BottomSheetUiState.kt index 7107a7c4395a..2f163dd1ecfc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/BottomSheetUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/BottomSheetUiState.kt @@ -5,7 +5,7 @@ import org.wordpress.android.ui.utils.UiString sealed class BottomSheetUiState(val isVisible: Boolean) { data class BottomSheetVisible( val title: UiString, - val categories: List + val category: SubfilterCategory ) : BottomSheetUiState(true) object BottomSheetHidden : BottomSheetUiState(false) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt index 44d50e65e150..e331ecaeb231 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubFilterViewModel.kt @@ -234,7 +234,7 @@ class SubFilterViewModel @Inject constructor( _bottomSheetUiState.value = Event( BottomSheetVisible( UiStringRes(category.titleRes), - listOf(category) // TODO thomashortadev this should accept only a single category + category ) ) val source = when(category) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt index 7e7d1bd07c77..4820df8c4599 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt @@ -4,9 +4,6 @@ package org.wordpress.android.ui.reader.subfilter import android.content.Context import android.os.Bundle -import android.os.Parcel -import android.os.Parcelable -import android.os.Parcelable.Creator import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -107,7 +104,7 @@ class SubfilterPageFragment : Fragment() { subFilterViewModel = ViewModelProvider( requireParentFragment().parentFragment as ViewModelStoreOwner, viewModelFactory - ).get(subfilterVmKey, SubFilterViewModel::class.java) + )[subfilterVmKey, SubFilterViewModel::class.java] subFilterViewModel.subFilters.observe(viewLifecycleOwner) { (recyclerView.adapter as? SubfilterListAdapter)?.let { adapter -> @@ -187,7 +184,7 @@ class SubfilterPageFragment : Fragment() { class SubfilterPagerAdapter( val context: Context, val fm: FragmentManager, - val subfilterViewModelKey: String, + private val subfilterViewModelKey: String, categories: List ) : FragmentPagerAdapter(fm) { private val filterCategory = categories @@ -201,7 +198,7 @@ class SubfilterPagerAdapter( return fragment } - override fun getPageTitle(position: Int): CharSequence? { + override fun getPageTitle(position: Int): CharSequence { return context.getString(filterCategory[position].titleRes) } @@ -215,25 +212,7 @@ class SubfilterPagerAdapter( } } -enum class SubfilterCategory(@StringRes val titleRes: Int, val type: ItemType) : Parcelable { +enum class SubfilterCategory(@StringRes val titleRes: Int, val type: ItemType) { SITES(R.string.reader_filter_by_blog_title, SITE), TAGS(R.string.reader_filter_by_tag_title, TAG); - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeInt(type.ordinal) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Creator { - override fun createFromParcel(parcel: Parcel): SubfilterCategory { - return values()[parcel.readInt()] - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt index 74984c278010..08881ec68a44 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt @@ -62,7 +62,9 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.T import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.TotalFollowersUseCase.TotalFollowersUseCaseFactory import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.TotalLikesUseCase.TotalLikesUseCaseFactory import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.ViewsAndVisitorsUseCase.ViewsAndVisitorsUseCaseFactory +import org.wordpress.android.ui.stats.refresh.utils.SelectedTrafficGranularityManager import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider +import org.wordpress.android.util.config.StatsTrafficTabFeatureConfig import javax.inject.Named import javax.inject.Singleton @@ -271,7 +273,6 @@ class StatsModule { * @param useCasesFactories build the use cases for the DAYS granularity */ @Provides - @Singleton @Named(TRAFFIC_USE_CASE) @Suppress("LongParameterList") fun provideTrafficUseCase( @@ -280,13 +281,16 @@ class StatsModule { @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, statsSiteProvider: StatsSiteProvider, @Named(GRANULAR_USE_CASE_FACTORIES) useCasesFactories: List<@JvmSuppressWildcards GranularUseCaseFactory>, + selectedTrafficGranularityManager: SelectedTrafficGranularityManager, uiModelMapper: UiModelMapper ): BaseListUseCase { return BaseListUseCase( bgDispatcher, mainDispatcher, statsSiteProvider, - useCasesFactories.map { it.build(DAYS, BLOCK) }, + useCasesFactories.map { + it.build(selectedTrafficGranularityManager.getSelectedTrafficGranularity(), BLOCK) + }, { statsStore.getTimeStatsTypes(it) }, uiModelMapper::mapTimeStats ) @@ -408,16 +412,20 @@ class StatsModule { @Named(DAY_STATS_USE_CASE) dayStatsUseCase: BaseListUseCase, @Named(WEEK_STATS_USE_CASE) weekStatsUseCase: BaseListUseCase, @Named(MONTH_STATS_USE_CASE) monthStatsUseCase: BaseListUseCase, - @Named(YEAR_STATS_USE_CASE) yearStatsUseCase: BaseListUseCase + @Named(YEAR_STATS_USE_CASE) yearStatsUseCase: BaseListUseCase, + trafficTabFeatureConfig: StatsTrafficTabFeatureConfig ): Map { - return mapOf( - StatsSection.INSIGHTS to insightsUseCase, - StatsSection.TRAFFIC to trafficUseCase, - StatsSection.DAYS to dayStatsUseCase, - StatsSection.WEEKS to weekStatsUseCase, - StatsSection.MONTHS to monthStatsUseCase, - StatsSection.YEARS to yearStatsUseCase - ) + return if (trafficTabFeatureConfig.isEnabled()) { + mapOf(StatsSection.TRAFFIC to trafficUseCase, StatsSection.INSIGHTS to insightsUseCase) + } else { + mapOf( + StatsSection.INSIGHTS to insightsUseCase, + StatsSection.DAYS to dayStatsUseCase, + StatsSection.WEEKS to weekStatsUseCase, + StatsSection.MONTHS to monthStatsUseCase, + StatsSection.YEARS to yearStatsUseCase + ) + } } /** diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/BaseListUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/BaseListUseCase.kt index b01c21f2abc7..a64406487935 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/BaseListUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/BaseListUseCase.kt @@ -22,8 +22,8 @@ import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.util.PackageUtils import org.wordpress.android.util.combineMap import org.wordpress.android.util.distinct -import org.wordpress.android.util.mapSafe import org.wordpress.android.util.mapAsync +import org.wordpress.android.util.mapSafe import org.wordpress.android.util.mergeAsyncNotNull import org.wordpress.android.util.mergeNotNull import org.wordpress.android.viewmodel.Event @@ -147,4 +147,13 @@ class BaseListUseCase( fun onListSelected() { mutableListSelected.call() } + + fun clone(newUseCases: List>) = BaseListUseCase( + bgDispatcher, + mainDispatcher, + statsSiteProvider, + newUseCases, + getStatsTypes, + mapUiModel + ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt index 1469325b5c26..c8d09a0b3643 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt @@ -81,15 +81,12 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { } } - @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) statsSection = arguments?.getSerializableCompat(LIST_TYPE) ?: activity?.intent?.getSerializableExtraCompat(LIST_TYPE) ?: StatsSection.INSIGHTS - - setHasOptionsMenu(statsSection == StatsSection.INSIGHTS) } override fun onSaveInstanceState(outState: Bundle) { @@ -169,7 +166,10 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { dateSelector.granularitySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - selectedTrafficGranularityManager.setSelectedTrafficGranularity(StatsGranularity.entries[position]) + with(StatsGranularity.entries[position]) { + selectedTrafficGranularityManager.setSelectedTrafficGranularity(this) + (viewModel as TrafficListViewModel).onGranularitySelected(this) + } } @Suppress("EmptyFunctionBlock") @@ -207,6 +207,12 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { } } + override fun onResume() { + super.onResume() + @Suppress("DEPRECATION") + setHasOptionsMenu(statsSection == StatsSection.INSIGHTS) + } + override fun onDestroyView() { super.onDestroyView() binding = null @@ -235,8 +241,15 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { } private fun StatsListFragmentBinding.setupObservers(activity: FragmentActivity) { - viewModel.uiModel.observe(viewLifecycleOwner) { - showUiModel(it) + viewModel.uiSourceRemoved.observe(viewLifecycleOwner) { + viewModel.uiModel.removeObservers(viewLifecycleOwner) + viewModel.navigationTarget.removeObservers(viewLifecycleOwner) + viewModel.listSelected.removeObservers(viewLifecycleOwner) + viewModel.scrollToNewCard.removeObservers(viewLifecycleOwner) + } + + viewModel.uiSourceAdded.observe(viewLifecycleOwner) { + observeUiChanges(activity) } viewModel.dateSelectorData.observe(viewLifecycleOwner) { dateSelectorUiModel -> @@ -248,20 +261,12 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { } } - viewModel.navigationTarget.observeEvent(viewLifecycleOwner) { target -> - navigator.navigate(activity, target) - } - viewModel.selectedDate?.observe(viewLifecycleOwner) { event -> if (event != null) { viewModel.onDateChanged(event.selectedGranularity) } } - viewModel.listSelected.observe(viewLifecycleOwner) { - viewModel.onListSelected() - } - viewModel.typesChanged.observeEvent(viewLifecycleOwner) { viewModel.onTypesChanged() } @@ -271,6 +276,16 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { recyclerView.smoothScrollToPosition(adapter.positionOf(statsType)) } } + } + + private fun StatsListFragmentBinding.observeUiChanges(activity: FragmentActivity) { + viewModel.uiModel.observe(viewLifecycleOwner) { + showUiModel(it) + } + + viewModel.navigationTarget.observeEvent(viewLifecycleOwner) { target -> navigator.navigate(activity, target) } + + viewModel.listSelected.observe(viewLifecycleOwner) { viewModel.onListSelected() } viewModel.scrollToNewCard.observeEvent(viewLifecycleOwner) { (recyclerView.adapter as? StatsBlockAdapter)?.let { adapter -> @@ -331,6 +346,7 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { val layoutManager = recyclerView.layoutManager val recyclerViewState = layoutManager?.onSaveInstanceState() adapter.update(statsState) + recyclerView.scrollToPosition(0) layoutManager?.onRestoreInstanceState(recyclerViewState) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt index 1643bedabaae..366d1f7073f1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt @@ -11,8 +11,10 @@ import kotlinx.coroutines.delay import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.fluxc.network.utils.StatsGranularity +import org.wordpress.android.fluxc.store.StatsStore import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.stats.refresh.DAY_STATS_USE_CASE +import org.wordpress.android.ui.stats.refresh.GRANULAR_USE_CASE_FACTORIES import org.wordpress.android.ui.stats.refresh.INSIGHTS_USE_CASE import org.wordpress.android.ui.stats.refresh.MONTH_STATS_USE_CASE import org.wordpress.android.ui.stats.refresh.NavigationTarget @@ -25,9 +27,12 @@ import org.wordpress.android.ui.stats.refresh.TRAFFIC_USE_CASE import org.wordpress.android.ui.stats.refresh.VIEWS_AND_VISITORS_USE_CASE import org.wordpress.android.ui.stats.refresh.WEEK_STATS_USE_CASE import org.wordpress.android.ui.stats.refresh.YEAR_STATS_USE_CASE +import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase +import org.wordpress.android.ui.stats.refresh.lists.sections.granular.GranularUseCaseFactory import org.wordpress.android.ui.stats.refresh.utils.ActionCardHandler import org.wordpress.android.ui.stats.refresh.utils.ItemPopupMenuHandler import org.wordpress.android.ui.stats.refresh.utils.NewsCardHandler +import org.wordpress.android.ui.stats.refresh.utils.SelectedTrafficGranularityManager import org.wordpress.android.ui.stats.refresh.utils.StatsDateSelector import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.mapNullable @@ -36,6 +41,7 @@ import org.wordpress.android.util.mergeNotNull import org.wordpress.android.util.throttle import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel +import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject import javax.inject.Named @@ -43,9 +49,9 @@ const val SCROLL_EVENT_DELAY = 2000L abstract class StatsListViewModel( defaultDispatcher: CoroutineDispatcher, - private val statsUseCase: BaseListUseCase, + protected var statsUseCase: BaseListUseCase, private val analyticsTracker: AnalyticsTrackerWrapper, - protected val dateSelector: StatsDateSelector?, + protected var dateSelector: StatsDateSelector?, popupMenuHandler: ItemPopupMenuHandler? = null, private val newsCardHandler: NewsCardHandler? = null, actionCardHandler: ActionCardHandler? = null @@ -71,15 +77,17 @@ abstract class StatsListViewModel( val selectedDate = dateSelector?.selectedDate private val mutableNavigationTarget = MutableLiveData>() - val navigationTarget: LiveData> = mergeNotNull( - statsUseCase.navigationTarget, mutableNavigationTarget - ) + lateinit var navigationTarget: LiveData> - val listSelected = statsUseCase.listSelected + lateinit var listSelected: LiveData - val uiModel: LiveData by lazy { - statsUseCase.data.throttle(viewModelScope, distinct = true) - } + private val mutableUiSourceAdded = SingleLiveEvent() + val uiSourceAdded: LiveData = mutableUiSourceAdded + + protected val mutableUiSourceRemoved = SingleLiveEvent() + val uiSourceRemoved: LiveData = mutableUiSourceRemoved + + lateinit var uiModel: LiveData val dateSelectorData: LiveData = dateSelector?.dateSelectorData?.mapNullable { it ?: DateSelectorUiModel(false) @@ -93,7 +101,7 @@ abstract class StatsListViewModel( val scrollTo = newsCardHandler?.scrollTo - val scrollToNewCard = statsUseCase.scrollTo + lateinit var scrollToNewCard: LiveData> override fun onCleared() { statsUseCase.onCleared() @@ -150,6 +158,7 @@ abstract class StatsListViewModel( fun start() { if (!isInitialized) { isInitialized = true + setUiLiveData() launch { statsUseCase.loadData() dateSelector?.updateDateSelector() @@ -158,6 +167,14 @@ abstract class StatsListViewModel( dateSelector?.updateDateSelector() } + protected fun setUiLiveData() { + uiModel = statsUseCase.data.throttle(viewModelScope, distinct = true) + listSelected = statsUseCase.listSelected + navigationTarget = mergeNotNull(statsUseCase.navigationTarget, mutableNavigationTarget) + scrollToNewCard = statsUseCase.scrollTo + mutableUiSourceAdded.call() + } + sealed class UiModel { data class Success(val data: List) : UiModel() data class Error(val message: Int = R.string.stats_loading_error) : UiModel() @@ -196,15 +213,44 @@ class InsightsListViewModel class TrafficListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, - @Named(TRAFFIC_USE_CASE) statsUseCase: BaseListUseCase, + @Named(TRAFFIC_USE_CASE) private val trafficStatsUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, - dateSelectorFactory: StatsDateSelector.Factory + dateSelectorFactory: StatsDateSelector.Factory, + @Named(GRANULAR_USE_CASE_FACTORIES) + private val useCasesFactories: List<@JvmSuppressWildcards GranularUseCaseFactory>, + private val selectedTrafficGranularityManager: SelectedTrafficGranularityManager, ) : StatsListViewModel( mainDispatcher, - statsUseCase, + trafficStatsUseCase, analyticsTracker, - dateSelectorFactory.build(StatsGranularity.DAYS, isGranularitySpinnerVisible = true) -) + dateSelectorFactory.build( + selectedTrafficGranularityManager.getSelectedTrafficGranularity(), + isGranularitySpinnerVisible = true + ) +) { + fun onGranularitySelected(statsGranularity: StatsGranularity) { + if (dateSelector?.statsGranularity != statsGranularity) { + // Remove observers from the UI before changing the statsUseCase. This prevents removed use cases from + // affecting the UI. + mutableUiSourceRemoved.call() + + dateSelector?.statsGranularity = statsGranularity + val newUseCases = useCasesFactories.map { + it.build( + selectedTrafficGranularityManager.getSelectedTrafficGranularity(), + BaseStatsUseCase.UseCaseMode.BLOCK + ) + } + statsUseCase.onCleared() + statsUseCase = statsUseCase.clone(newUseCases) // Create new BaseListUseCase with updated useCases + launch { + statsUseCase.loadData() + dateSelector?.updateDateSelector() + } + setUiLiveData() // Set UI live data and observers again + } + } +} class YearsListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt index 1ce18f0db9b7..c4e5c0879c9a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt @@ -16,7 +16,6 @@ import org.wordpress.android.ui.stats.refresh.utils.trackWithGranularity import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.extensions.readListCompat -import org.wordpress.android.util.filter import java.util.Date import javax.inject.Inject import javax.inject.Singleton @@ -38,9 +37,7 @@ class SelectedDateProvider private val selectedDateChanged = MutableLiveData() - fun granularSelectedDateChanged(statsGranularity: StatsGranularity): LiveData { - return selectedDateChanged.filter { it?.selectedGranularity == statsGranularity } - } + fun granularSelectedDateChanged(): LiveData = selectedDateChanged fun selectDate(date: Date, statsGranularity: StatsGranularity) { val selectedDate = getSelectedDateState(statsGranularity) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt index bf7d89e37a20..1e092d4f2812 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt @@ -15,17 +15,18 @@ constructor( private val selectedDateProvider: SelectedDateProvider, private val statsDateFormatter: StatsDateFormatter, private val siteProvider: StatsSiteProvider, - private val statsGranularity: StatsGranularity, + var statsGranularity: StatsGranularity, private val isGranularitySpinnerVisible: Boolean, private val statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig ) { private val _dateSelectorUiModel = MutableLiveData() val dateSelectorData: LiveData = _dateSelectorUiModel - val selectedDate = selectedDateProvider.granularSelectedDateChanged(statsGranularity) - .perform { + var selectedDate = selectedDateProvider.granularSelectedDateChanged().perform { + if (statsGranularity == it?.selectedGranularity) { updateDateSelector() } + } fun start(startDate: SelectedDate) { selectedDateProvider.updateSelectedDate(startDate, statsGranularity) diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 62e87667048c..f7531852e2c6 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2006,6 +2006,8 @@ This file type is not allowed Unexpected empty file path for Media Could not find media file in path + Unable to load the media for sharing. Please check the app\'s permissions + or use the app\'s media library. The specified path is a directory instead of a Media file Media upload failed.\n%1$s Your action is not allowed diff --git a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt index af7fd0adf5f2..48ba3faf6764 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt @@ -44,7 +44,7 @@ class StatsDateSelectorTest : BaseUnitTest() { @Before fun setUp() { dateProviderSelectedDate.value = GranularityChange(statsGranularity) - whenever(selectedDateProvider.granularSelectedDateChanged(statsGranularity)) + whenever(selectedDateProvider.granularSelectedDateChanged()) .thenReturn(dateProviderSelectedDate) dateSelector = StatsDateSelector( diff --git a/build.gradle b/build.gradle index ece11ae91c13..1d393ace05cd 100644 --- a/build.gradle +++ b/build.gradle @@ -22,13 +22,13 @@ ext { automatticAboutVersion = '1.4.0' automatticRestVersion = '1.0.8' automatticStoriesVersion = '2.4.0' - automatticTracksVersion = '3.3.0' + automatticTracksVersion = '3.4.0' gutenbergMobileVersion = 'v1.112.0' wordPressAztecVersion = 'v2.0' wordPressFluxCVersion = 'trunk-ed60798b4d96ec19863c74b0f525e2e20f4525db' wordPressLoginVersion = 'trunk-a90b1ce939aba700d822f188d41624385f9c1dce' wordPressPersistentEditTextVersion = '1.0.2' - wordPressUtilsVersion = '3.12.0' + wordPressUtilsVersion = '3.13.0' indexosMediaForMobileVersion = '43a9026f0973a2f0a74fa813132f6a16f7499c3a' // debug @@ -59,7 +59,7 @@ ext { androidxSwipeToRefreshVersion = '1.1.0' androidxViewpager2Version = '1.0.0' androidxWorkManagerVersion = "2.8.1" - androidxWebkitVersion = '1.7.0' + androidxWebkitVersion = '1.10.0' androidxComposeMaterial3Version = '1.1.1' apacheCommonsTextVersion = '1.10.0' coilComposeVersion = '2.4.0' diff --git a/fastlane/resources/values/strings.xml b/fastlane/resources/values/strings.xml index d28bdcc5957d..9b1825e36e98 100644 --- a/fastlane/resources/values/strings.xml +++ b/fastlane/resources/values/strings.xml @@ -2006,6 +2006,8 @@ This file type is not allowed Unexpected empty file path for Media Could not find media file in path + Unable to load the media for sharing. Please check the app\'s permissions + or use the app\'s media library. The specified path is a directory instead of a Media file Media upload failed.\n%1$s Your action is not allowed diff --git a/settings.gradle b/settings.gradle index 5e48b42287db..0dd34fb91ca1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,7 @@ pluginManagement { gradle.ext.agpVersion = '8.1.0' gradle.ext.googleServicesVersion = '4.3.15' gradle.ext.navigationVersion = '2.5.3' - gradle.ext.sentryVersion = '3.5.0' + gradle.ext.sentryVersion = '3.14.0' gradle.ext.daggerVersion = "2.46.1" gradle.ext.detektVersion = '1.23.0' gradle.ext.violationCommentsVersion = '1.67'