From 22ab728be0f2a070f074f9578252c3c5b97a5881 Mon Sep 17 00:00:00 2001 From: Andy Valdez Date: Thu, 18 Jan 2024 16:07:45 -0500 Subject: [PATCH 01/38] [Bug] Made updates related to targetSdk update for media. I omitted the targetSdk update in this PR simply to allow testing with the previous version to ensure everything remains the same. Additionally, I added the foreground service types related to images. --- WordPress/src/main/AndroidManifest.xml | 7 +++++-- .../wordpress/android/ui/media/MediaSettingsActivity.java | 8 +++++++- .../android/ui/mediapicker/loader/DeviceListBuilder.kt | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 56d049968bb8..f06af72f1ec4 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ + @@ -823,11 +824,13 @@ + android:label="Upload Service" + android:foregroundServiceType="dataSync"/> + android:exported="false" > diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java index 15672b09cd48..ba86d66631a8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java @@ -9,6 +9,7 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; @@ -16,6 +17,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Environment; import android.os.Handler; @@ -467,7 +469,11 @@ protected void onSaveInstanceState(@NonNull Bundle outState) { @SuppressLint("UnspecifiedRegisterReceiverFlag") public void onStart() { super.onStart(); - registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + if (Build.VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) { + registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), ContextWrapper.RECEIVER_NOT_EXPORTED); + } else { + registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + } mDispatcher.register(this); // we only register with EventBus the first time - necessary since we don't unregister in onStop() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt index c9549d250ec1..99be2e0f8dc7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt @@ -69,7 +69,7 @@ class DeviceListBuilder( // This item sets the threshold for the visible items in all the list val lastShownTimestamp = results.fold(0L) { timestamp, (_, result) -> val nextTimestamp = result?.nextTimestamp - if (nextTimestamp != null && nextTimestamp > timestamp) { + if (result?.items?.isNotEmpty() == true && nextTimestamp != null && nextTimestamp > timestamp) { nextTimestamp } else { timestamp From eaa406c7f9bfb9448835aebcc8524c3429ed6def Mon Sep 17 00:00:00 2001 From: Andy Valdez Date: Thu, 18 Jan 2024 17:24:46 -0500 Subject: [PATCH 02/38] [Fix] Add foreground service types to all our manifest defined services --- WordPress/src/main/AndroidManifest.xml | 57 +++++++++++++++++--------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index f06af72f1ec4..1477bacbe66f 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -836,88 +836,105 @@ + android:label="Reader Update Service" + android:foregroundServiceType="dataSync"/> + android:label="Reader Update JobService" + android:foregroundServiceType="dataSync"/> + android:label="Reader Discover Service" + android:foregroundServiceType="dataSync"/> + android:label="Reader Discover JobService" + android:foregroundServiceType="dataSync"/> + android:label="Reader Post Service" + android:foregroundServiceType="dataSync"/> + android:label="Reader Post JobService" + android:foregroundServiceType="dataSync"/> + android:label="Reader Search Service" + android:foregroundServiceType="dataSync"/> + android:label="Reader Search Job Service" + android:foregroundServiceType="dataSync"/> + android:label="Reader Comment Service" + android:foregroundServiceType="dataSync"/> + android:label="Suggestion Service" + android:foregroundServiceType="dataSync"/> + android:label="Notifications Quick Actions processing Service" + android:foregroundServiceType="dataSync"/> + android:label="Notifications Update Service" + android:foregroundServiceType="dataSync"/> + android:label="Notifications Update Job Service" + android:foregroundServiceType="dataSync"/> + android:label="Installation Referrer Service" + android:foregroundServiceType="dataSync"/> + android:label="Installation Referrer Service" + android:foregroundServiceType="dataSync"/> + android:label="Login to WPCOM Service" + android:foregroundServiceType="dataSync"/> + android:label="Site Creation Service" + android:foregroundServiceType="dataSync"/> + android:permission="android.permission.BIND_REMOTEVIEWS" + android:foregroundServiceType="dataSync"/> + android:exported="false" + android:foregroundServiceType="dataSync"> From b1ebd86c366f6d26b4ac0030104b46b1b89b25d6 Mon Sep 17 00:00:00 2001 From: Andy Valdez Date: Wed, 24 Jan 2024 12:49:56 -0500 Subject: [PATCH 03/38] [Bug] Move a small bug fix to a more appropriate place. --- .../android/ui/mediapicker/loader/DeviceListBuilder.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt index 99be2e0f8dc7..2627f5c008f0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/loader/DeviceListBuilder.kt @@ -69,7 +69,7 @@ class DeviceListBuilder( // This item sets the threshold for the visible items in all the list val lastShownTimestamp = results.fold(0L) { timestamp, (_, result) -> val nextTimestamp = result?.nextTimestamp - if (result?.items?.isNotEmpty() == true && nextTimestamp != null && nextTimestamp > timestamp) { + if (nextTimestamp != null && nextTimestamp > timestamp) { nextTimestamp } else { timestamp @@ -118,7 +118,7 @@ class DeviceListBuilder( null } } - addPage(mediaType, result, deviceMediaList.next) + addPage(mediaType, result, if (result.isEmpty()) null else deviceMediaList.next) return cache[mediaType] } From 6c50eec550809926ddfc2c37a609a1810da985ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=81ctor=20Abraham?= Date: Mon, 12 Feb 2024 16:56:46 +0100 Subject: [PATCH 04/38] Added Gravatar message in "My profile" Showing the card with the Gravatar info in the My Profile fragment. The card should contain a link to the Gravartar.com website. Design differs between light and dark modes. --- .../src/jetpack/res/drawable/ic_logo.xml | 15 ++ .../android/ui/prefs/MyProfileFragment.java | 6 + .../drawable/bg_wordpress_gravatar_info.xml | 7 + .../main/res/drawable/ic_logo_gravatar.xml | 14 ++ .../main/res/layout/my_profile_fragment.xml | 156 ++++++++++++------ WordPress/src/main/res/values/dimens.xml | 4 + WordPress/src/main/res/values/strings.xml | 2 + .../wordpress/res/drawable-night/ic_logo.xml | 15 ++ .../src/wordpress/res/drawable/ic_logo.xml | 15 ++ 9 files changed, 184 insertions(+), 50 deletions(-) create mode 100644 WordPress/src/jetpack/res/drawable/ic_logo.xml create mode 100644 WordPress/src/main/res/drawable/bg_wordpress_gravatar_info.xml create mode 100644 WordPress/src/main/res/drawable/ic_logo_gravatar.xml create mode 100644 WordPress/src/wordpress/res/drawable-night/ic_logo.xml create mode 100644 WordPress/src/wordpress/res/drawable/ic_logo.xml diff --git a/WordPress/src/jetpack/res/drawable/ic_logo.xml b/WordPress/src/jetpack/res/drawable/ic_logo.xml new file mode 100644 index 000000000000..4d7225822e2c --- /dev/null +++ b/WordPress/src/jetpack/res/drawable/ic_logo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java index e9b9b1dd5181..ac4654cbd53e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java @@ -5,6 +5,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.TextView; import androidx.annotation.Nullable; @@ -22,6 +23,7 @@ import org.wordpress.android.fluxc.store.AccountStore; import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged; import org.wordpress.android.fluxc.store.AccountStore.PushAccountSettingsPayload; +import org.wordpress.android.ui.ActivityLauncher; import org.wordpress.android.ui.TextInputDialogFragment; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; @@ -37,6 +39,7 @@ public class MyProfileFragment extends Fragment implements TextInputDialogFragme private WPTextView mLastName; private WPTextView mDisplayName; private WPTextView mAboutMe; + private Button mLearMoreAtGravatar; @Inject Dispatcher mDispatcher; @Inject AccountStore mAccountStore; @@ -44,6 +47,7 @@ public class MyProfileFragment extends Fragment implements TextInputDialogFragme private static final String TRACK_PROPERTY_FIELD_NAME = "field_name"; private static final String TRACK_PROPERTY_PAGE = "page"; private static final String TRACK_PROPERTY_PAGE_MY_PROFILE = "my_profile"; + private static final String GRAVATAR_URL = "https://www.gravatar.com"; public static MyProfileFragment newInstance() { return new MyProfileFragment(); @@ -85,6 +89,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mLastName = rootView.findViewById(R.id.last_name); mDisplayName = rootView.findViewById(R.id.display_name); mAboutMe = rootView.findViewById(R.id.about_me); + mLearMoreAtGravatar = rootView.findViewById(R.id.learn_more_at_gravatar); rootView.findViewById(R.id.first_name_row).setOnClickListener( createOnClickListener( @@ -110,6 +115,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa getString(R.string.about_me_hint), mAboutMe, true)); + mLearMoreAtGravatar.setOnClickListener(v -> ActivityLauncher.openUrlExternal(getActivity(), GRAVATAR_URL)); return rootView; } diff --git a/WordPress/src/main/res/drawable/bg_wordpress_gravatar_info.xml b/WordPress/src/main/res/drawable/bg_wordpress_gravatar_info.xml new file mode 100644 index 000000000000..3d6c96044319 --- /dev/null +++ b/WordPress/src/main/res/drawable/bg_wordpress_gravatar_info.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/WordPress/src/main/res/drawable/ic_logo_gravatar.xml b/WordPress/src/main/res/drawable/ic_logo_gravatar.xml new file mode 100644 index 000000000000..57ea54c36b70 --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_logo_gravatar.xml @@ -0,0 +1,14 @@ + + + + diff --git a/WordPress/src/main/res/layout/my_profile_fragment.xml b/WordPress/src/main/res/layout/my_profile_fragment.xml index c18f9810d3c1..c9a1955f8264 100644 --- a/WordPress/src/main/res/layout/my_profile_fragment.xml +++ b/WordPress/src/main/res/layout/my_profile_fragment.xml @@ -1,79 +1,135 @@ - + android:layout_height="match_parent"> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + - + - + + + + + + + + + + + + - + + + - + diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index 80496b1f890f..b3c131621c03 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -763,4 +763,8 @@ 4dp 24dp 4dp + + + 30dp + -10dp diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index f7531852e2c6..f3887828dd20 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2869,6 +2869,8 @@ You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site. Show purchases Checking purchases + "Gravatar keeps your profile information safe and up to date, automatically syncing any updates made here with your Gravatar profile." + Learn more on Gravatar.com Account Settings diff --git a/WordPress/src/wordpress/res/drawable-night/ic_logo.xml b/WordPress/src/wordpress/res/drawable-night/ic_logo.xml new file mode 100644 index 000000000000..a69d5abb5312 --- /dev/null +++ b/WordPress/src/wordpress/res/drawable-night/ic_logo.xml @@ -0,0 +1,15 @@ + + + + diff --git a/WordPress/src/wordpress/res/drawable/ic_logo.xml b/WordPress/src/wordpress/res/drawable/ic_logo.xml new file mode 100644 index 000000000000..c7770d87517e --- /dev/null +++ b/WordPress/src/wordpress/res/drawable/ic_logo.xml @@ -0,0 +1,15 @@ + + + + From 68441f1bb0fc5a8d3f10bdefb221f339d9ea90c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=81ctor=20Abraham?= Date: Mon, 12 Feb 2024 17:39:08 +0100 Subject: [PATCH 05/38] Added Gravatar Sync Info When the user updates any of the fields, we need some time until the change is propagated to Gravatar. We want to inform the user about that. Also, the user can dismiss that notification with a done button. --- .../android/ui/prefs/MyProfileFragment.java | 6 +++ .../main/res/layout/my_profile_fragment.xml | 48 ++++++++++++++++++- WordPress/src/main/res/values/strings.xml | 2 + 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java index ac4654cbd53e..01ded25760d5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java @@ -40,6 +40,8 @@ public class MyProfileFragment extends Fragment implements TextInputDialogFragme private WPTextView mDisplayName; private WPTextView mAboutMe; private Button mLearMoreAtGravatar; + private Button mGravatarSyncButton; + private View mGravatarSyncContainer; @Inject Dispatcher mDispatcher; @Inject AccountStore mAccountStore; @@ -90,6 +92,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mDisplayName = rootView.findViewById(R.id.display_name); mAboutMe = rootView.findViewById(R.id.about_me); mLearMoreAtGravatar = rootView.findViewById(R.id.learn_more_at_gravatar); + mGravatarSyncButton = rootView.findViewById(R.id.gravatar_sync_button); + mGravatarSyncContainer = rootView.findViewById(R.id.gravatar_sync_container); rootView.findViewById(R.id.first_name_row).setOnClickListener( createOnClickListener( @@ -116,6 +120,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mAboutMe, true)); mLearMoreAtGravatar.setOnClickListener(v -> ActivityLauncher.openUrlExternal(getActivity(), GRAVATAR_URL)); + mGravatarSyncButton.setOnClickListener(v -> mGravatarSyncContainer.setVisibility(View.GONE)); return rootView; } @@ -180,6 +185,7 @@ private void updateMyProfileForLabel(TextView textView) { payload.params.put(restParamForTextView(textView), textView.getText().toString()); mDispatcher.dispatch(AccountActionBuilder.newPushSettingsAction(payload)); trackSettingsDidChange(restParamForTextView(textView)); + mGravatarSyncContainer.setVisibility(View.VISIBLE); } private void trackSettingsDidChange(String fieldName) { diff --git a/WordPress/src/main/res/layout/my_profile_fragment.xml b/WordPress/src/main/res/layout/my_profile_fragment.xml index c9a1955f8264..e9f62391db70 100644 --- a/WordPress/src/main/res/layout/my_profile_fragment.xml +++ b/WordPress/src/main/res/layout/my_profile_fragment.xml @@ -1,8 +1,10 @@ + android:layout_height="match_parent" + android:animateLayoutChanges="true"> @@ -132,4 +135,45 @@ app:icon="@drawable/ic_external_white_24dp" /> + + + + + + + + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index f3887828dd20..94489959c095 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2871,6 +2871,8 @@ Checking purchases "Gravatar keeps your profile information safe and up to date, automatically syncing any updates made here with your Gravatar profile." Learn more on Gravatar.com + Updates might take some time to sync with your Gravatar profile. + Done Account Settings From 512697e2dcf31392a9b7717c6ba022421eac220a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=81ctor=20Abraham?= Date: Wed, 14 Feb 2024 09:35:15 +0100 Subject: [PATCH 06/38] Remove unnecessary quotes in string --- WordPress/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 94489959c095..7f51471a4dd7 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2869,7 +2869,7 @@ You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site. Show purchases Checking purchases - "Gravatar keeps your profile information safe and up to date, automatically syncing any updates made here with your Gravatar profile." + Gravatar keeps your profile information safe and up to date, automatically syncing any updates made here with your Gravatar profile. Learn more on Gravatar.com Updates might take some time to sync with your Gravatar profile. Done From 6b2cc45e8d6d9c81fad9d163d26db9501571e91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=81ctor=20Abraham?= Date: Wed, 14 Feb 2024 11:44:43 +0100 Subject: [PATCH 07/38] Fixing profile fields list constraint in MyProfileFragment The list height was wrap_content, so when the space was limited with the new Gravatar view, the list was unusable. It kept behind the Gravatar view. This was visible, for example, in landscape or with a bigger text size. Adding the constraint to the top of the Gravatar view should minimize the issue. There are still some corners case where the UX can be improved. --- WordPress/src/main/res/layout/my_profile_fragment.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/res/layout/my_profile_fragment.xml b/WordPress/src/main/res/layout/my_profile_fragment.xml index e9f62391db70..191a0038fc57 100644 --- a/WordPress/src/main/res/layout/my_profile_fragment.xml +++ b/WordPress/src/main/res/layout/my_profile_fragment.xml @@ -9,7 +9,9 @@ From e33f86581e85d0e66ae4140d8a0ffcd921d8c599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?He=CC=81ctor=20Abraham?= Date: Fri, 16 Feb 2024 11:07:06 +0100 Subject: [PATCH 08/38] Move Gravatar info banner from "MyProfileFragment" to "MeFragment" After reviewing with designers, we want to show the informative banner on the first screen as users can modify their avatar in the "Profile" fragment. The sync information banner will be shown in both fragments, MyProfile and MeFragment, after any modification on the Gravatar profile. The sync banner is reused, so we've extracted it to a separate XML layout. This commit mainly moves the logic and view from the MeFragment to the MyProfile one. --- .../drawable-night/ic_logo_plus_gravatar.xml | 28 ++++++ .../src/jetpack/res/drawable/ic_logo.xml | 15 ---- .../res/drawable/ic_logo_plus_gravatar.xml | 28 ++++++ .../wordpress/android/ui/main/MeFragment.kt | 8 ++ .../android/ui/prefs/MyProfileFragment.java | 5 -- .../drawable/bg_wordpress_gravatar_info.xml | 2 +- .../bg_wordpress_gravatar_sync_info.xml | 7 ++ .../main/res/drawable/ic_logo_gravatar.xml | 14 --- .../res/layout/gravatar_sync_info_banner.xml | 40 +++++++++ WordPress/src/main/res/layout/me_fragment.xml | 71 ++++++++++++++- .../main/res/layout/my_profile_fragment.xml | 86 +------------------ .../src/main/res/values-night/colors.xml | 4 + .../src/main/res/values-night/styles.xml | 3 + WordPress/src/main/res/values/attrs.xml | 4 + WordPress/src/main/res/values/colors.xml | 4 + WordPress/src/main/res/values/dimens.xml | 3 - WordPress/src/main/res/values/strings.xml | 5 +- WordPress/src/main/res/values/styles.xml | 3 + .../wordpress/res/drawable-night/ic_logo.xml | 15 ---- .../drawable-night/ic_logo_plus_gravatar.xml | 24 ++++++ .../src/wordpress/res/drawable/ic_logo.xml | 15 ---- .../res/drawable/ic_logo_plus_gravatar.xml | 24 ++++++ 22 files changed, 255 insertions(+), 153 deletions(-) create mode 100644 WordPress/src/jetpack/res/drawable-night/ic_logo_plus_gravatar.xml delete mode 100644 WordPress/src/jetpack/res/drawable/ic_logo.xml create mode 100644 WordPress/src/jetpack/res/drawable/ic_logo_plus_gravatar.xml create mode 100644 WordPress/src/main/res/drawable/bg_wordpress_gravatar_sync_info.xml delete mode 100644 WordPress/src/main/res/drawable/ic_logo_gravatar.xml create mode 100644 WordPress/src/main/res/layout/gravatar_sync_info_banner.xml delete mode 100644 WordPress/src/wordpress/res/drawable-night/ic_logo.xml create mode 100644 WordPress/src/wordpress/res/drawable-night/ic_logo_plus_gravatar.xml delete mode 100644 WordPress/src/wordpress/res/drawable/ic_logo.xml create mode 100644 WordPress/src/wordpress/res/drawable/ic_logo_plus_gravatar.xml diff --git a/WordPress/src/jetpack/res/drawable-night/ic_logo_plus_gravatar.xml b/WordPress/src/jetpack/res/drawable-night/ic_logo_plus_gravatar.xml new file mode 100644 index 000000000000..466aaf0e0ff3 --- /dev/null +++ b/WordPress/src/jetpack/res/drawable-night/ic_logo_plus_gravatar.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/WordPress/src/jetpack/res/drawable/ic_logo.xml b/WordPress/src/jetpack/res/drawable/ic_logo.xml deleted file mode 100644 index 4d7225822e2c..000000000000 --- a/WordPress/src/jetpack/res/drawable/ic_logo.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/WordPress/src/jetpack/res/drawable/ic_logo_plus_gravatar.xml b/WordPress/src/jetpack/res/drawable/ic_logo_plus_gravatar.xml new file mode 100644 index 000000000000..d7d7e4bac4ac --- /dev/null +++ b/WordPress/src/jetpack/res/drawable/ic_logo_plus_gravatar.xml @@ -0,0 +1,28 @@ + + + + + + + + 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 index 0528ccae8684..347e700f21d3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MeFragment.kt @@ -200,6 +200,12 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { rowSupport.setOnClickListener { ActivityLauncher.viewHelp(requireContext(), ME_SCREEN_HELP, viewModel.getSite(), null) } + learnMoreAtGravatar.setOnClickListener { + ActivityLauncher.openUrlExternal(activity, GRAVATAR_URL) + } + gravatarSyncView.gravatarSyncButton.setOnClickListener { + gravatarSyncView.gravatarSyncContainer.visibility = View.GONE + } if (BuildConfig.IS_JETPACK_APP) meAboutIcon.setImageResource(R.drawable.ic_jetpack_logo_white_24dp) @@ -691,6 +697,7 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { if (event.success) { AnalyticsTracker.track(ME_GRAVATAR_UPLOADED) binding?.loadAvatar(event.filePath) + binding?.gravatarSyncView?.gravatarSyncContainer?.visibility = View.VISIBLE } else { ToastUtils.showToast( activity, @@ -709,6 +716,7 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener { companion object { private const val IS_DISCONNECTING = "IS_DISCONNECTING" private const val IS_UPDATING_GRAVATAR = "IS_UPDATING_GRAVATAR" + private const val GRAVATAR_URL = "https://www.gravatar.com"; fun newInstance(): MeFragment { return MeFragment() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java index 01ded25760d5..d880322609a2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileFragment.java @@ -23,7 +23,6 @@ import org.wordpress.android.fluxc.store.AccountStore; import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged; import org.wordpress.android.fluxc.store.AccountStore.PushAccountSettingsPayload; -import org.wordpress.android.ui.ActivityLauncher; import org.wordpress.android.ui.TextInputDialogFragment; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; @@ -39,7 +38,6 @@ public class MyProfileFragment extends Fragment implements TextInputDialogFragme private WPTextView mLastName; private WPTextView mDisplayName; private WPTextView mAboutMe; - private Button mLearMoreAtGravatar; private Button mGravatarSyncButton; private View mGravatarSyncContainer; @@ -49,7 +47,6 @@ public class MyProfileFragment extends Fragment implements TextInputDialogFragme private static final String TRACK_PROPERTY_FIELD_NAME = "field_name"; private static final String TRACK_PROPERTY_PAGE = "page"; private static final String TRACK_PROPERTY_PAGE_MY_PROFILE = "my_profile"; - private static final String GRAVATAR_URL = "https://www.gravatar.com"; public static MyProfileFragment newInstance() { return new MyProfileFragment(); @@ -91,7 +88,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mLastName = rootView.findViewById(R.id.last_name); mDisplayName = rootView.findViewById(R.id.display_name); mAboutMe = rootView.findViewById(R.id.about_me); - mLearMoreAtGravatar = rootView.findViewById(R.id.learn_more_at_gravatar); mGravatarSyncButton = rootView.findViewById(R.id.gravatar_sync_button); mGravatarSyncContainer = rootView.findViewById(R.id.gravatar_sync_container); @@ -119,7 +115,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa getString(R.string.about_me_hint), mAboutMe, true)); - mLearMoreAtGravatar.setOnClickListener(v -> ActivityLauncher.openUrlExternal(getActivity(), GRAVATAR_URL)); mGravatarSyncButton.setOnClickListener(v -> mGravatarSyncContainer.setVisibility(View.GONE)); return rootView; diff --git a/WordPress/src/main/res/drawable/bg_wordpress_gravatar_info.xml b/WordPress/src/main/res/drawable/bg_wordpress_gravatar_info.xml index 3d6c96044319..f9b6826d3195 100644 --- a/WordPress/src/main/res/drawable/bg_wordpress_gravatar_info.xml +++ b/WordPress/src/main/res/drawable/bg_wordpress_gravatar_info.xml @@ -1,7 +1,7 @@ - + diff --git a/WordPress/src/main/res/drawable/bg_wordpress_gravatar_sync_info.xml b/WordPress/src/main/res/drawable/bg_wordpress_gravatar_sync_info.xml new file mode 100644 index 000000000000..0920b9a9a1af --- /dev/null +++ b/WordPress/src/main/res/drawable/bg_wordpress_gravatar_sync_info.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/WordPress/src/main/res/drawable/ic_logo_gravatar.xml b/WordPress/src/main/res/drawable/ic_logo_gravatar.xml deleted file mode 100644 index 57ea54c36b70..000000000000 --- a/WordPress/src/main/res/drawable/ic_logo_gravatar.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/WordPress/src/main/res/layout/gravatar_sync_info_banner.xml b/WordPress/src/main/res/layout/gravatar_sync_info_banner.xml new file mode 100644 index 000000000000..4a3d104edd90 --- /dev/null +++ b/WordPress/src/main/res/layout/gravatar_sync_info_banner.xml @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/WordPress/src/main/res/layout/me_fragment.xml b/WordPress/src/main/res/layout/me_fragment.xml index 0661d0dc88b1..d7adbad671d2 100644 --- a/WordPress/src/main/res/layout/me_fragment.xml +++ b/WordPress/src/main/res/layout/me_fragment.xml @@ -238,7 +238,7 @@ android:id="@+id/me_design_system_settings" style="@style/MeListRowTextView" android:text="@string/preference_design_system" /> - + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/my_profile_fragment.xml b/WordPress/src/main/res/layout/my_profile_fragment.xml index 191a0038fc57..5cf6c337bf60 100644 --- a/WordPress/src/main/res/layout/my_profile_fragment.xml +++ b/WordPress/src/main/res/layout/my_profile_fragment.xml @@ -11,7 +11,7 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginBottom="@dimen/margin_small" - app:layout_constraintBottom_toTopOf="@+id/gravatar_info_container" + app:layout_constraintBottom_toTopOf="@+id/gravatar_sync_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -90,92 +90,14 @@ - - - - - - - - - - - - - - - - - - - - - + tools:visibility="visible" /> diff --git a/WordPress/src/main/res/values-night/colors.xml b/WordPress/src/main/res/values-night/colors.xml index 4fed9a9fe846..ff2b633e7bde 100644 --- a/WordPress/src/main/res/values-night/colors.xml +++ b/WordPress/src/main/res/values-night/colors.xml @@ -115,4 +115,8 @@ @color/black @color/white + + #1C1C1E + #2C2C2E + diff --git a/WordPress/src/main/res/values-night/styles.xml b/WordPress/src/main/res/values-night/styles.xml index 686c482b8cf9..54e8280b64c1 100644 --- a/WordPress/src/main/res/values-night/styles.xml +++ b/WordPress/src/main/res/values-night/styles.xml @@ -30,6 +30,9 @@ @color/background_dark_elevated ?attr/colorSurface + @color/gravatar_info_banner + @color/gravatar_sync_info_banner + @color/material_on_surface_emphasis_medium ?attr/colorPrimary @color/on_surface_emphasis_lowest_disabled diff --git a/WordPress/src/main/res/values/attrs.xml b/WordPress/src/main/res/values/attrs.xml index c6eec240f04c..4c496caa5e9c 100644 --- a/WordPress/src/main/res/values/attrs.xml +++ b/WordPress/src/main/res/values/attrs.xml @@ -22,6 +22,10 @@ + + + + diff --git a/WordPress/src/main/res/values/colors.xml b/WordPress/src/main/res/values/colors.xml index 808a85d7e5d7..dabb3e1b6711 100644 --- a/WordPress/src/main/res/values/colors.xml +++ b/WordPress/src/main/res/values/colors.xml @@ -150,4 +150,8 @@ #DEDEDE + + #FAFAFA + #2C2C2E + diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index b3c131621c03..f67241180b4f 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -764,7 +764,4 @@ 24dp 4dp - - 30dp - -10dp diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 7f51471a4dd7..dc4df0c3b0cc 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2869,8 +2869,9 @@ You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site. Show purchases Checking purchases - Gravatar keeps your profile information safe and up to date, automatically syncing any updates made here with your Gravatar profile. - Learn more on Gravatar.com + Your WordPress.com profile is powered by Gravatar + Updating your avatar, name, and about info here will also update it across all sites that use Gravatar profiles. + What is Gravatar? Updates might take some time to sync with your Gravatar profile. Done diff --git a/WordPress/src/main/res/values/styles.xml b/WordPress/src/main/res/values/styles.xml index 6166eb13334b..de43d8f78c93 100644 --- a/WordPress/src/main/res/values/styles.xml +++ b/WordPress/src/main/res/values/styles.xml @@ -49,6 +49,9 @@ @color/neutral_5 @color/blue_0 + @color/gravatar_info_banner + @color/gravatar_sync_info_banner + @color/wp_grey_lighten_30 @android:color/white diff --git a/WordPress/src/wordpress/res/drawable-night/ic_logo.xml b/WordPress/src/wordpress/res/drawable-night/ic_logo.xml deleted file mode 100644 index a69d5abb5312..000000000000 --- a/WordPress/src/wordpress/res/drawable-night/ic_logo.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/WordPress/src/wordpress/res/drawable-night/ic_logo_plus_gravatar.xml b/WordPress/src/wordpress/res/drawable-night/ic_logo_plus_gravatar.xml new file mode 100644 index 000000000000..0bb208c0899c --- /dev/null +++ b/WordPress/src/wordpress/res/drawable-night/ic_logo_plus_gravatar.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/WordPress/src/wordpress/res/drawable/ic_logo.xml b/WordPress/src/wordpress/res/drawable/ic_logo.xml deleted file mode 100644 index c7770d87517e..000000000000 --- a/WordPress/src/wordpress/res/drawable/ic_logo.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/WordPress/src/wordpress/res/drawable/ic_logo_plus_gravatar.xml b/WordPress/src/wordpress/res/drawable/ic_logo_plus_gravatar.xml new file mode 100644 index 000000000000..ab99e681d54d --- /dev/null +++ b/WordPress/src/wordpress/res/drawable/ic_logo_plus_gravatar.xml @@ -0,0 +1,24 @@ + + + + + + From 445fbf64e30e50dda0a7c91690b94bce5289b692 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 10:53:33 +0800 Subject: [PATCH 09/38] Remove a redundant API request --- .../NotificationsListFragment.kt | 23 ------------------- 1 file changed, 23 deletions(-) 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 index 183284ba4cb9..3eafa87352d5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt @@ -58,12 +58,10 @@ import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS.FILT 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.ui.utils.UiHelpers import org.wordpress.android.util.JetpackBrandingUtils -import org.wordpress.android.util.NetworkUtils import org.wordpress.android.util.PermissionUtils import org.wordpress.android.util.WPPermissionUtils import org.wordpress.android.util.WPPermissionUtils.NOTIFICATIONS_PERMISSION_REQUEST_CODE @@ -89,7 +87,6 @@ class NotificationsListFragment : Fragment(R.layout.notifications_list_fragment) private val viewModel: NotificationsListViewModel by viewModels() - private var shouldRefreshNotifications = false private var lastTabPosition = 0 private var binding: NotificationsListFragmentBinding? = null @@ -101,11 +98,6 @@ class NotificationsListFragment : Fragment(R.layout.notifications_list_fragment) } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - shouldRefreshNotifications = true - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setHasOptionsMenu(true) @@ -161,11 +153,6 @@ class NotificationsListFragment : Fragment(R.layout.notifications_list_fragment) } } - override fun onPause() { - super.onPause() - shouldRefreshNotifications = true - } - override fun onDestroyView() { super.onDestroyView() binding = null @@ -184,9 +171,6 @@ class NotificationsListFragment : Fragment(R.layout.notifications_list_fragment) connectJetpack.visibility = View.GONE tabLayout.visibility = View.VISIBLE viewPager.visibility = View.VISIBLE - if (shouldRefreshNotifications) { - fetchNotesFromRemote() - } } setSelectedTab(lastTabPosition) setNotificationPermissionWarning() @@ -206,13 +190,6 @@ class NotificationsListFragment : Fragment(R.layout.notifications_list_fragment) } } - private fun fetchNotesFromRemote() { - if (!isAdded || !NetworkUtils.isNetworkAvailable(activity)) { - return - } - NotificationsUpdateServiceStarter.startService(activity) - } - private fun NotificationsListFragmentBinding.setSelectedTab(position: Int) { lastTabPosition = position tabLayout.getTabAt(lastTabPosition)?.select() From 1841f4903b75c9a1df9726d693d52308a2c7556b Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 10:56:45 +0800 Subject: [PATCH 10/38] Refactor with ViewBinding --- .../ui/notifications/adapters/NotesAdapter.kt | 136 +++++++----------- 1 file changed, 51 insertions(+), 85 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 9770c061875d..6e3d03749fdd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -1,5 +1,3 @@ -@file:Suppress("DEPRECATION") - package org.wordpress.android.ui.notifications.adapters import android.content.Context @@ -28,6 +26,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.wordpress.android.R import org.wordpress.android.WordPress +import org.wordpress.android.databinding.NotificationsListItemBinding import org.wordpress.android.datasets.NotificationsTable import org.wordpress.android.models.Note import org.wordpress.android.models.Note.NoteTimeGroup @@ -125,9 +124,7 @@ class NotesAdapter( } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder = - LayoutInflater.from(parent.context) - .inflate(R.layout.notifications_list_item, parent, false) - .let { NoteViewHolder(it) } + NoteViewHolder(NotificationsListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)) private fun getNoteAtPosition(position: Int): Note? = if (isValidPosition(position)) { filteredNotes[position] @@ -163,16 +160,16 @@ class NotesAdapter( override fun onBindViewHolder(noteViewHolder: NoteViewHolder, position: Int) { val note = getNoteAtPosition(position) ?: return val previousNote = getNoteAtPosition(position - 1) - noteViewHolder.contentView.tag = note.id + noteViewHolder.binding.noteContentContainer.tag = note.id // Display time group header timeGroupHeaderText(note, previousNote)?.let { timeGroupText -> - with(noteViewHolder.headerText) { + with(noteViewHolder.binding.headerText) { visibility = View.VISIBLE setText(timeGroupText) } } ?: run { - noteViewHolder.headerText.visibility = View.GONE + noteViewHolder.binding.headerText.visibility = View.GONE } // Subject is stored in db as html to preserve text formatting @@ -188,40 +185,40 @@ class NotesAdapter( NoteBlockClickableSpan::class.java ) for (span in spans) { - span.enableColors(noteViewHolder.contentView.context) + span.enableColors(noteViewHolder.itemView.context) } - noteViewHolder.textSubject.text = noteSubjectSpanned + noteViewHolder.binding.noteSubject.text = noteSubjectSpanned val noteSubjectNoticon = note.commentSubjectNoticon if (!TextUtils.isEmpty(noteSubjectNoticon)) { - val parent = noteViewHolder.textSubject.parent + val parent = noteViewHolder.binding.noteSubject.parent // Fix position of the subject noticon in the RtL mode if (parent is ViewGroup) { val textDirection = if (BidiFormatter.getInstance() - .isRtl(noteViewHolder.textSubject.text) + .isRtl(noteViewHolder.binding.noteSubject.text) ) ViewCompat.LAYOUT_DIRECTION_RTL else ViewCompat.LAYOUT_DIRECTION_LTR ViewCompat.setLayoutDirection(parent, textDirection) } // mirror noticon in the rtl mode if (RtlUtils.isRtl(noteViewHolder.itemView.context)) { - noteViewHolder.textSubjectNoticon.scaleX = -1f + noteViewHolder.binding.noteSubjectNoticon.scaleX = -1f } - CommentUtils.indentTextViewFirstLine(noteViewHolder.textSubject, textIndentSize) - noteViewHolder.textSubjectNoticon.text = noteSubjectNoticon - noteViewHolder.textSubjectNoticon.visibility = View.VISIBLE + CommentUtils.indentTextViewFirstLine(noteViewHolder.binding.noteSubject, textIndentSize) + noteViewHolder.binding.noteSubjectNoticon.text = noteSubjectNoticon + noteViewHolder.binding.noteSubjectNoticon.visibility = View.VISIBLE } else { - noteViewHolder.textSubjectNoticon.visibility = View.GONE + noteViewHolder.binding.noteSubjectNoticon.visibility = View.GONE } val noteSnippet = note.commentSubject if (!TextUtils.isEmpty(noteSnippet)) { - handleMaxLines(noteViewHolder.textSubject, noteViewHolder.textDetail) - noteViewHolder.textDetail.text = noteSnippet - noteViewHolder.textDetail.visibility = View.VISIBLE + handleMaxLines(noteViewHolder.binding.noteSubject, noteViewHolder.binding.noteDetail) + noteViewHolder.binding.noteDetail.text = noteSnippet + noteViewHolder.binding.noteDetail.visibility = View.VISIBLE } else { - noteViewHolder.textDetail.visibility = View.GONE + noteViewHolder.binding.noteDetail.visibility = View.GONE } noteViewHolder.loadAvatars(note) noteViewHolder.bindInlineActionIconsForNote(note) - noteViewHolder.unreadNotificationView.isVisible = note.isUnread + noteViewHolder.binding.notificationUnread.isVisible = note.isUnread // request to load more comments when we near the end if (onLoadMoreListener != null && position >= itemCount - 1) { @@ -236,33 +233,33 @@ class NotesAdapter( context.resources .getDimensionPixelSize(R.dimen.notifications_header_margin_top_position_n) } - val layoutParams = noteViewHolder.headerText.layoutParams as MarginLayoutParams + val layoutParams = noteViewHolder.binding.headerText.layoutParams as MarginLayoutParams layoutParams.topMargin = headerMarginTop - noteViewHolder.headerText.layoutParams = layoutParams + noteViewHolder.binding.headerText.layoutParams = layoutParams } private fun NoteViewHolder.loadAvatars(note: Note) { if (note.shouldShowMultipleAvatars() && note.iconURLs != null && note.iconURLs!!.size > 1) { val avatars = note.iconURLs!!.toList() if (avatars.size == 2) { - imageAvatar.visibility = View.INVISIBLE - twoAvatarsView.visibility = View.VISIBLE - threeAvatarsView.visibility = View.INVISIBLE - loadAvatar(twoAvatars1, avatars[1]) - loadAvatar(twoAvatars2, avatars[0]) + binding.noteAvatar.visibility = View.INVISIBLE + binding.twoAvatarsView.root.visibility = View.VISIBLE + binding.threeAvatarsView.root.visibility = View.INVISIBLE + loadAvatar(binding.twoAvatarsView.twoAvatars1, avatars[1]) + loadAvatar(binding.twoAvatarsView.twoAvatars2, avatars[0]) } else { // size > 3 - imageAvatar.visibility = View.INVISIBLE - twoAvatarsView.visibility = View.INVISIBLE - threeAvatarsView.visibility = View.VISIBLE - loadAvatar(threeAvatars1, avatars[2]) - loadAvatar(threeAvatars2, avatars[1]) - loadAvatar(threeAvatars3, avatars[0]) + binding.noteAvatar.visibility = View.INVISIBLE + binding.twoAvatarsView.root.visibility = View.INVISIBLE + binding.threeAvatarsView.root.visibility = View.VISIBLE + loadAvatar(binding.threeAvatarsView.threeAvatars1, avatars[2]) + loadAvatar(binding.threeAvatarsView.threeAvatars2, avatars[1]) + loadAvatar(binding.threeAvatarsView.threeAvatars3, avatars[0]) } } else { // single avatar - imageAvatar.visibility = View.VISIBLE - twoAvatarsView.visibility = View.INVISIBLE - threeAvatarsView.visibility = View.INVISIBLE - loadAvatar(imageAvatar, note.iconURL) + binding.noteAvatar.visibility = View.VISIBLE + binding.twoAvatarsView.root.visibility = View.INVISIBLE + binding.threeAvatarsView.root.visibility = View.INVISIBLE + loadAvatar(binding.noteAvatar, note.iconURL) } } @@ -317,40 +314,9 @@ class NotesAdapter( } } - inner class NoteViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val contentView: View - val headerText: TextView - val textSubject: TextView - val textSubjectNoticon: TextView - val textDetail: TextView - val imageAvatar: ImageView - val twoAvatarsView: View - val twoAvatars1: ImageView - val twoAvatars2: ImageView - val threeAvatarsView: View - val threeAvatars1: ImageView - val threeAvatars2: ImageView - val threeAvatars3: ImageView - val unreadNotificationView: View - val actionIcon: ImageView - + inner class NoteViewHolder(val binding: NotificationsListItemBinding) : RecyclerView.ViewHolder(binding.root) { init { - contentView = checkNotNull(view.findViewById(R.id.note_content_container)) - headerText = checkNotNull(view.findViewById(R.id.header_text)) - textSubject = checkNotNull(view.findViewById(R.id.note_subject)) - textSubjectNoticon = checkNotNull(view.findViewById(R.id.note_subject_noticon)) - textDetail = checkNotNull(view.findViewById(R.id.note_detail)) - imageAvatar = checkNotNull(view.findViewById(R.id.note_avatar)) - twoAvatars1 = checkNotNull(view.findViewById(R.id.two_avatars_1)) - twoAvatars2 = checkNotNull(view.findViewById(R.id.two_avatars_2)) - threeAvatars1 = checkNotNull(view.findViewById(R.id.three_avatars_1)) - threeAvatars2 = checkNotNull(view.findViewById(R.id.three_avatars_2)) - threeAvatars3 = checkNotNull(view.findViewById(R.id.three_avatars_3)) - twoAvatarsView = checkNotNull(view.findViewById(R.id.two_avatars_view)) - threeAvatarsView = checkNotNull(view.findViewById(R.id.three_avatars_view)) - unreadNotificationView = checkNotNull(view.findViewById(R.id.notification_unread)) - actionIcon = checkNotNull(view.findViewById(R.id.action)) - contentView.setOnClickListener(onClickListener) + binding.noteContentContainer.setOnClickListener(onClickListener) } fun bindInlineActionIconsForNote(note: Note) = Notification.from(note).let { notification -> @@ -359,17 +325,17 @@ class NotesAdapter( is PostNotification.NewPost -> bindLikePostAction(note) is PostNotification -> bindShareAction(notification) is Unknown -> { - actionIcon.isVisible = false + binding.action.isVisible = false } } } private fun bindShareAction(notification: PostNotification) { - actionIcon.setImageResource(R.drawable.block_share) - val color = contentView.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) - ImageViewCompat.setImageTintList(actionIcon, ColorStateList.valueOf(color)) - actionIcon.isVisible = true - actionIcon.setOnClickListener { + binding.action.setImageResource(R.drawable.block_share) + val color = binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) + ImageViewCompat.setImageTintList(binding.action, ColorStateList.valueOf(color)) + binding.action.isVisible = true + binding.action.setOnClickListener { coroutineScope.launch { inlineActionEvents.emit( InlineActionEvent.SharePostButtonTapped(notification) @@ -381,7 +347,7 @@ class NotesAdapter( private fun bindLikePostAction(note: Note) { if (note.canLikePost().not()) return setupLikeIcon(note.hasLikedPost()) - actionIcon.setOnClickListener { + binding.action.setOnClickListener { val liked = note.hasLikedPost().not() setupLikeIcon(liked) coroutineScope.launch { @@ -395,7 +361,7 @@ class NotesAdapter( private fun bindLikeCommentAction(note: Note) { if (note.canLikeComment().not()) return setupLikeIcon(note.hasLikedComment()) - actionIcon.setOnClickListener { + binding.action.setOnClickListener { val liked = note.hasLikedComment().not() setupLikeIcon(liked) coroutineScope.launch { @@ -407,11 +373,11 @@ class NotesAdapter( } private fun setupLikeIcon(liked: Boolean) { - actionIcon.isVisible = true - actionIcon.setImageResource(if (liked) R.drawable.star_filled else R.drawable.star_empty) - val color = if (liked) contentView.context.getColor(R.color.inline_action_filled) - else contentView.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) - ImageViewCompat.setImageTintList(actionIcon, ColorStateList.valueOf(color)) + binding.action.isVisible = true + binding.action.setImageResource(if (liked) R.drawable.star_filled else R.drawable.star_empty) + val color = if (liked) binding.root.context.getColor(R.color.inline_action_filled) + else binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) + ImageViewCompat.setImageTintList(binding.action, ColorStateList.valueOf(color)) } } From 42435ead7e3238dda2ee6701a96f67a71716f6bf Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 15:40:36 +0800 Subject: [PATCH 11/38] Refactor position-related functions in adapter --- .../ui/notifications/adapters/NotesAdapter.kt | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 6e3d03749fdd..82014a18e391 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -16,7 +16,8 @@ import androidx.core.text.BidiFormatter import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.core.widget.ImageViewCompat -import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil.ItemCallback import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -105,20 +106,18 @@ class NotesAdapter( */ fun addAll(notes: List) = coroutineScope.launch { val newNotes = buildFilteredNotesList(notes, currentFilter) - val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { - override fun getOldListSize(): Int = filteredNotes.size - override fun getNewListSize(): Int = newNotes.size - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - filteredNotes[oldItemPosition].id == newNotes[newItemPosition].id - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - filteredNotes[oldItemPosition].json.toString() == newNotes[newItemPosition].json.toString() + val diff = AsyncListDiffer(this@NotesAdapter, object: ItemCallback(){ + override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean = + oldItem.id == newItem.id + + override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean = + oldItem.json.toString() == newItem.json.toString() }) filteredNotes.clear() filteredNotes.addAll(newNotes) withContext(Dispatchers.Main) { - result.dispatchUpdatesTo(this@NotesAdapter) + diff.submitList(newNotes) dataLoadedListener.onDataLoaded(itemCount) } } @@ -126,12 +125,6 @@ class NotesAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder = NoteViewHolder(NotificationsListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - private fun getNoteAtPosition(position: Int): Note? = if (isValidPosition(position)) { - filteredNotes[position] - } else null - - private fun isValidPosition(position: Int): Boolean = position >= 0 && position < filteredNotes.size - override fun getItemCount(): Int = filteredNotes.size private val Note.timeGroup @@ -158,8 +151,8 @@ class NotesAdapter( @Suppress("CyclomaticComplexMethod", "LongMethod") override fun onBindViewHolder(noteViewHolder: NoteViewHolder, position: Int) { - val note = getNoteAtPosition(position) ?: return - val previousNote = getNoteAtPosition(position - 1) + val note = filteredNotes.getOrNull(position) ?: return + val previousNote = filteredNotes.getOrNull(position - 1) noteViewHolder.binding.noteContentContainer.tag = note.id // Display time group header From 6d8aaa7f974b34db6a83736d92045536d404c569 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 16:52:48 +0800 Subject: [PATCH 12/38] Remove OnNoteClickListener --- .../NotificationsListFragmentPage.kt | 64 +++++++++---------- .../ui/notifications/adapters/NotesAdapter.kt | 23 ++----- 2 files changed, 33 insertions(+), 54 deletions(-) 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 index 934f213ee0c1..3cbf5d3ba99c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -100,10 +100,6 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l private var binding: NotificationsListFragmentPageBinding? = null - interface OnNoteClickListener { - fun onClickNote(noteId: String?) - } - @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == RequestCodes.NOTE_DETAIL) { @@ -127,9 +123,11 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l arguments?.let { tabPosition = it.getInt(KEY_TAB_POSITION, All.ordinal) } - notesAdapter = NotesAdapter( requireActivity(), this, null, - inlineActionEvents = viewModel.inlineActionEvents).apply { - this.setOnNoteClickListener(mOnNoteClickListener) + notesAdapter = NotesAdapter( + requireActivity(), this, null, + inlineActionEvents = viewModel.inlineActionEvents + ).apply { + onNoteClicked = { noteId -> handleNoteClick(noteId) } viewModel.inlineActionEvents.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) .onEach(::handleInlineActionEvent) .launchIn(viewLifecycleOwner.lifecycleScope) @@ -220,36 +218,32 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l 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) - - viewModel.openNote( - noteId, - { siteId, postId, commentId -> - ReaderActivityLauncher.showReaderComments( - activity, - siteId, - postId, - commentId, - ThreadedCommentsActionSource.COMMENT_NOTIFICATION.sourceDescription - ) - }, - { - // 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 the - // NotificationsDetailListFragment). - openNoteForReply(activity, noteId, filter = notesAdapter.currentFilter) - } - ) + private fun handleNoteClick(noteId: String) { + if (!isAdded || noteId.isEmpty()) { + return } + incrementInteractions(APP_REVIEWS_EVENT_INCREMENTED_BY_CHECKING_NOTIFICATION) + + viewModel.openNote( + noteId, + { siteId, postId, commentId -> + ReaderActivityLauncher.showReaderComments( + activity, + siteId, + postId, + commentId, + ThreadedCommentsActionSource.COMMENT_NOTIFICATION.sourceDescription + ) + }, + { + // 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 the + // NotificationsDetailListFragment). + openNoteForReply(activity, noteId, filter = notesAdapter.currentFilter) + } + ) } + private val mOnScrollListener: OnScrollListener = object : OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 82014a18e391..21f0e6c9ac84 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -36,7 +36,6 @@ import org.wordpress.android.models.Notification.Comment import org.wordpress.android.models.Notification.PostNotification import org.wordpress.android.models.Notification.Unknown import org.wordpress.android.ui.comments.CommentUtils -import org.wordpress.android.ui.notifications.NotificationsListFragmentPage.OnNoteClickListener import org.wordpress.android.ui.notifications.NotificationsListViewModel.InlineActionEvent import org.wordpress.android.ui.notifications.adapters.NotesAdapter.NoteViewHolder import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan @@ -59,6 +58,7 @@ class NotesAdapter( private val onLoadMoreListener: OnLoadMoreListener? private val coroutineScope = CoroutineScope(Dispatchers.IO) val filteredNotes = ArrayList() + var onNoteClicked = { _: String -> } @Inject lateinit var imageManager: ImageManager @@ -96,7 +96,6 @@ class NotesAdapter( fun onLoadMore(timestamp: Long) } - private var onNoteClickListener: OnNoteClickListener? = null fun setFilter(newFilter: FILTERS) { currentFilter = newFilter } @@ -106,7 +105,7 @@ class NotesAdapter( */ fun addAll(notes: List) = coroutineScope.launch { val newNotes = buildFilteredNotesList(notes, currentFilter) - val diff = AsyncListDiffer(this@NotesAdapter, object: ItemCallback(){ + val differ = AsyncListDiffer(this@NotesAdapter, object: ItemCallback(){ override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean = oldItem.id == newItem.id @@ -117,7 +116,7 @@ class NotesAdapter( filteredNotes.clear() filteredNotes.addAll(newNotes) withContext(Dispatchers.Main) { - diff.submitList(newNotes) + differ.submitList(newNotes) dataLoadedListener.onDataLoaded(itemCount) } } @@ -153,7 +152,7 @@ class NotesAdapter( override fun onBindViewHolder(noteViewHolder: NoteViewHolder, position: Int) { val note = filteredNotes.getOrNull(position) ?: return val previousNote = filteredNotes.getOrNull(position - 1) - noteViewHolder.binding.noteContentContainer.tag = note.id + noteViewHolder.binding.noteContentContainer.setOnClickListener { onNoteClicked(note.id) } // Display time group header timeGroupHeaderText(note, previousNote)?.let { timeGroupText -> @@ -277,10 +276,6 @@ class NotesAdapter( }) } - fun setOnNoteClickListener(mNoteClickListener: OnNoteClickListener?) { - onNoteClickListener = mNoteClickListener - } - fun cancelReloadLocalNotes() { reloadLocalNotesJob?.cancel() } @@ -308,10 +303,6 @@ class NotesAdapter( } inner class NoteViewHolder(val binding: NotificationsListItemBinding) : RecyclerView.ViewHolder(binding.root) { - init { - binding.noteContentContainer.setOnClickListener(onClickListener) - } - fun bindInlineActionIconsForNote(note: Note) = Notification.from(note).let { notification -> when (notification) { Comment -> bindLikeCommentAction(note) @@ -374,12 +365,6 @@ class NotesAdapter( } } - private val onClickListener = View.OnClickListener { view -> - if (onNoteClickListener != null && view.tag is String) { - onNoteClickListener!!.onClickNote(view.tag as String) - } - } - init { (context.applicationContext as WordPress).component().inject(this) this.dataLoadedListener = dataLoadedListener From 1e331558d7940e6d431aef4ff14aa1ee0e943c7a Mon Sep 17 00:00:00 2001 From: Maxime Biais Date: Mon, 19 Feb 2024 10:00:56 +0100 Subject: [PATCH 13/38] Fix for widow words --- WordPress/src/main/res/layout/me_fragment.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WordPress/src/main/res/layout/me_fragment.xml b/WordPress/src/main/res/layout/me_fragment.xml index d7adbad671d2..c1820e85576a 100644 --- a/WordPress/src/main/res/layout/me_fragment.xml +++ b/WordPress/src/main/res/layout/me_fragment.xml @@ -420,6 +420,7 @@ From d53a304487ad3ff1056756b1e303be8dcfdf3e00 Mon Sep 17 00:00:00 2001 From: Maxime Biais Date: Mon, 19 Feb 2024 10:01:30 +0100 Subject: [PATCH 14/38] Update Gravatar info text size and background color on light theme --- WordPress/src/main/res/layout/me_fragment.xml | 3 +-- WordPress/src/main/res/values/colors.xml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/res/layout/me_fragment.xml b/WordPress/src/main/res/layout/me_fragment.xml index c1820e85576a..14a5ba98377e 100644 --- a/WordPress/src/main/res/layout/me_fragment.xml +++ b/WordPress/src/main/res/layout/me_fragment.xml @@ -423,8 +423,7 @@ app:fixWidowWords="true" android:layout_marginTop="@dimen/margin_extra_large" android:text="@string/gravatar_info_title" - android:textAppearance="?attr/textAppearanceHeadline6" - android:textStyle="bold" /> + android:textStyle="bold"/> #DEDEDE - #FAFAFA + #F2F2F7 #2C2C2E From d2126fb09a31d386d2e005b051f2d884ef354194 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 17:14:28 +0800 Subject: [PATCH 15/38] Make refactors --- .../NotificationsListFragmentPage.kt | 14 ++--- .../ui/notifications/adapters/NotesAdapter.kt | 56 +++++++------------ 2 files changed, 26 insertions(+), 44 deletions(-) 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 index 3cbf5d3ba99c..7380176aa54f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -56,7 +56,6 @@ import org.wordpress.android.ui.notifications.NotificationsListFragment.Companio import org.wordpress.android.ui.notifications.NotificationsListViewModel.InlineActionEvent import org.wordpress.android.ui.notifications.NotificationsListViewModel.InlineActionEvent.SharePostButtonTapped 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.services.NotificationsUpdateServiceStarter import org.wordpress.android.ui.notifications.utils.NotificationsActions @@ -75,8 +74,7 @@ import javax.inject.Inject @AndroidEntryPoint class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_list_fragment_page), - OnScrollToTopListener, - DataLoadedListener { + OnScrollToTopListener { private lateinit var notesAdapter: NotesAdapter private var swipeToRefreshHelper: SwipeToRefreshHelper? = null private var isAnimatingOutNewNotificationsBar = false @@ -100,7 +98,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l private var binding: NotificationsListFragmentPageBinding? = null - @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") + @Suppress("OVERRIDE_DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == RequestCodes.NOTE_DETAIL) { if (resultCode == Activity.RESULT_OK) { @@ -123,11 +121,9 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l arguments?.let { tabPosition = it.getInt(KEY_TAB_POSITION, All.ordinal) } - notesAdapter = NotesAdapter( - requireActivity(), this, null, - inlineActionEvents = viewModel.inlineActionEvents - ).apply { + notesAdapter = NotesAdapter(requireActivity(), inlineActionEvents = viewModel.inlineActionEvents).apply { onNoteClicked = { noteId -> handleNoteClick(noteId) } + onNotesLoaded = { itemCount -> updateEmptyLayouts(itemCount) } viewModel.inlineActionEvents.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) .onEach(::handleInlineActionEvent) .launchIn(viewLifecycleOwner.lifecycleScope) @@ -164,7 +160,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l binding = null } - override fun onDataLoaded(itemsCount: Int) { + private fun updateEmptyLayouts(itemsCount: Int) { if (!isAdded) { AppLog.d( T.NOTIFS, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 21f0e6c9ac84..2011743d63d6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -46,19 +46,15 @@ import org.wordpress.android.util.extensions.getColorFromAttribute import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.image.ImageType import javax.inject.Inject +import kotlin.math.roundToInt -class NotesAdapter( - context: Context, dataLoadedListener: DataLoadedListener, - onLoadMoreListener: OnLoadMoreListener?, - private val inlineActionEvents: MutableSharedFlow, -) : RecyclerView.Adapter() { - private val avatarSize: Int - private val textIndentSize: Int - private val dataLoadedListener: DataLoadedListener - private val onLoadMoreListener: OnLoadMoreListener? +class NotesAdapter(context: Context, private val inlineActionEvents: MutableSharedFlow) : + RecyclerView.Adapter() { private val coroutineScope = CoroutineScope(Dispatchers.IO) val filteredNotes = ArrayList() var onNoteClicked = { _: String -> } + var onNotesLoaded = { _: Int -> } + var onScrolledToBottom = { _:Long -> } @Inject lateinit var imageManager: ImageManager @@ -66,6 +62,16 @@ class NotesAdapter( @Inject lateinit var notificationsUtilsWrapper: NotificationsUtilsWrapper + init { + (context.applicationContext as WordPress).component().inject(this) + + // this is on purpose - we don't show more than a hundred or so notifications at a time so no need to set + // stable IDs. This helps prevent crashes in case a note comes with no ID (we've code checking for that + // elsewhere, but telling the RecyclerView.Adapter the notes have stable Ids and then failing to provide them + // will make things go south as in https://github.com/wordpress-mobile/WordPress-Android/issues/8741 + setHasStableIds(false) + } + enum class FILTERS { FILTER_ALL, FILTER_COMMENT, @@ -88,14 +94,6 @@ class NotesAdapter( private set private var reloadLocalNotesJob: Job? = null - interface DataLoadedListener { - fun onDataLoaded(itemsCount: Int) - } - - interface OnLoadMoreListener { - fun onLoadMore(timestamp: Long) - } - fun setFilter(newFilter: FILTERS) { currentFilter = newFilter } @@ -117,7 +115,7 @@ class NotesAdapter( filteredNotes.addAll(newNotes) withContext(Dispatchers.Main) { differ.submitList(newNotes) - dataLoadedListener.onDataLoaded(itemCount) + onNotesLoaded(itemCount) } } @@ -194,6 +192,8 @@ class NotesAdapter( if (RtlUtils.isRtl(noteViewHolder.itemView.context)) { noteViewHolder.binding.noteSubjectNoticon.scaleX = -1f } + val textIndentSize = noteViewHolder.itemView.context.resources + .getDimensionPixelSize(R.dimen.notifications_text_indent_sz) CommentUtils.indentTextViewFirstLine(noteViewHolder.binding.noteSubject, textIndentSize) noteViewHolder.binding.noteSubjectNoticon.text = noteSubjectNoticon noteViewHolder.binding.noteSubjectNoticon.visibility = View.VISIBLE @@ -213,8 +213,8 @@ class NotesAdapter( noteViewHolder.binding.notificationUnread.isVisible = note.isUnread // request to load more comments when we near the end - if (onLoadMoreListener != null && position >= itemCount - 1) { - onLoadMoreListener.onLoadMore(note.timestamp) + if (position >= itemCount - 1) { + onScrolledToBottom(note.timestamp) } val headerMarginTop: Int val context = noteViewHolder.itemView.context @@ -256,6 +256,7 @@ class NotesAdapter( } private fun loadAvatar(imageView: ImageView, avatarUrl: String) { + val avatarSize = imageView.context.resources.getDimension(R.dimen.notifications_avatar_sz).roundToInt() val url = GravatarUtils.fixGravatarUrl(avatarUrl, avatarSize) imageManager.loadIntoCircle(imageView, ImageType.AVATAR_WITH_BACKGROUND, url) } @@ -365,21 +366,6 @@ class NotesAdapter( } } - init { - (context.applicationContext as WordPress).component().inject(this) - this.dataLoadedListener = dataLoadedListener - this.onLoadMoreListener = onLoadMoreListener - - // this is on purpose - we don't show more than a hundred or so notifications at a time so no need to set - // stable IDs. This helps prevent crashes in case a note comes with no ID (we've code checking for that - // elsewhere, but telling the RecyclerView.Adapter the notes have stable Ids and then failing to provide them - // will make things go south as in https://github.com/wordpress-mobile/WordPress-Android/issues/8741 - setHasStableIds(false) - avatarSize = context.resources.getDimension(R.dimen.notifications_avatar_sz).toInt() - textIndentSize = - context.resources.getDimensionPixelSize(R.dimen.notifications_text_indent_sz) - } - companion object { // Instead of building the filtered notes list dynamically, create it once and re-use it. // Otherwise it's re-created so many times during layout. From ba1274fabee430a381222ef83a8b5d730abdd878 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 17:41:40 +0800 Subject: [PATCH 16/38] Extract inner classes --- .../android/ui/main/WPMainActivity.java | 4 +- .../NotificationsDetailActivity.java | 7 +- .../NotificationsListFragment.kt | 21 +- .../NotificationsListFragmentPage.kt | 6 +- .../ui/notifications/adapters/NotesAdapter.kt | 185 +++++++++--------- 5 files changed, 107 insertions(+), 116 deletions(-) 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 3d432ded823f..4b8d8395aebf 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 @@ -110,7 +110,7 @@ import org.wordpress.android.ui.notifications.NotificationsListFragment; import org.wordpress.android.ui.notifications.NotificationsListViewModel; import org.wordpress.android.ui.notifications.SystemNotificationsTracker; -import org.wordpress.android.ui.notifications.adapters.NotesAdapter; +import org.wordpress.android.ui.notifications.adapters.Filter; import org.wordpress.android.ui.notifications.receivers.NotificationsPendingDraftsReceiver; import org.wordpress.android.ui.notifications.utils.NotificationsActions; import org.wordpress.android.ui.notifications.utils.NotificationsUtils; @@ -1087,7 +1087,7 @@ public Unit invoke() { NotificationsListFragment.NOTE_INSTANT_REPLY_EXTRA, false); NotificationsListFragment.openNoteForReply(WPMainActivity.this, noteId, - shouldShowKeyboard, null, NotesAdapter.FILTERS.FILTER_ALL, true); + shouldShowKeyboard, null, Filter.ALL, true); return null; } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java index ff6fb5747d57..fd0234d368d4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java @@ -43,6 +43,7 @@ import org.wordpress.android.ui.comments.CommentDetailFragment; import org.wordpress.android.ui.engagement.EngagedPeopleListFragment; import org.wordpress.android.ui.engagement.ListScenarioUtils; +import org.wordpress.android.ui.notifications.adapters.Filter; import org.wordpress.android.ui.notifications.adapters.NotesAdapter; import org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter; import org.wordpress.android.ui.notifications.utils.NotificationsActions; @@ -203,9 +204,9 @@ private void updateUIAndNote(boolean doRefresh) { } } - NotesAdapter.FILTERS filter = NotesAdapter.FILTERS.FILTER_ALL; + Filter filter = Filter.ALL; if (getIntent().hasExtra(NotificationsListFragment.NOTE_CURRENT_LIST_FILTER_EXTRA)) { - filter = (NotesAdapter.FILTERS) getIntent() + filter = (Filter) getIntent() .getSerializableExtra(NotificationsListFragment.NOTE_CURRENT_LIST_FILTER_EXTRA); } @@ -371,7 +372,7 @@ private void setActionBarTitleForNote(Note note) { } private NotificationDetailFragmentAdapter buildNoteListAdapterAndSetPosition(Note note, - NotesAdapter.FILTERS filter) { + Filter filter) { NotificationDetailFragmentAdapter adapter; ArrayList notes = NotificationsTable.getLatestNotes(); 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 index 3eafa87352d5..389a1bdcb613 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt @@ -52,12 +52,7 @@ import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredBottomSheetFra import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsUnseenStatus import org.wordpress.android.ui.notifications.NotificationsListFragment.Companion.TabPosition.All import org.wordpress.android.ui.notifications.NotificationsListFragmentPage.Companion.KEY_TAB_POSITION -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.adapters.Filter import org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter.IS_TAPPED_ON_NOTIFICATION import org.wordpress.android.ui.stats.StatsConnectJetpackActivity import org.wordpress.android.ui.utils.UiHelpers @@ -322,12 +317,12 @@ class NotificationsListFragment : Fragment(R.layout.notifications_list_fragment) const val NOTE_MODERATE_STATUS_EXTRA = "moderateNoteStatus" const val NOTE_CURRENT_LIST_FILTER_EXTRA = "currentFilter" - enum class TabPosition(@StringRes val titleRes: Int, val filter: FILTERS) { - All(R.string.notifications_tab_title_all, FILTER_ALL), - Unread(R.string.notifications_tab_title_unread_notifications, FILTER_UNREAD), - Comment(R.string.notifications_tab_title_comments, FILTER_COMMENT), - Follow(R.string.notifications_tab_title_follows, FILTER_FOLLOW), - Like(R.string.notifications_tab_title_likes, FILTER_LIKE); + enum class TabPosition(@StringRes val titleRes: Int, val filter: Filter) { + All(R.string.notifications_tab_title_all, Filter.ALL), + Unread(R.string.notifications_tab_title_unread_notifications, Filter.UNREAD), + Comment(R.string.notifications_tab_title_comments, Filter.COMMENT), + Follow(R.string.notifications_tab_title_follows, Filter.FOLLOW), + Like(R.string.notifications_tab_title_likes, Filter.LIKE); } private const val KEY_LAST_TAB_POSITION = "lastTabPosition" @@ -348,7 +343,7 @@ class NotificationsListFragment : Fragment(R.layout.notifications_list_fragment) noteId: String?, shouldShowKeyboard: Boolean, replyText: String?, - filter: FILTERS?, + filter: Filter?, isTappedFromPushNotification: Boolean ) { if (noteId == null || activity == null) { 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 index 7380176aa54f..d370fbf4284d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -55,8 +55,8 @@ import org.wordpress.android.ui.notifications.NotificationsListFragment.Companio import org.wordpress.android.ui.notifications.NotificationsListFragment.Companion.TabPosition.Unread import org.wordpress.android.ui.notifications.NotificationsListViewModel.InlineActionEvent import org.wordpress.android.ui.notifications.NotificationsListViewModel.InlineActionEvent.SharePostButtonTapped +import org.wordpress.android.ui.notifications.adapters.Filter 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.ui.notifications.utils.NotificationsActions import org.wordpress.android.ui.reader.ReaderActivityLauncher @@ -137,7 +137,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l } layoutNewNotificatons.visibility = View.GONE layoutNewNotificatons.setOnClickListener { onScrollToTop() } - (TabPosition.values().getOrNull(tabPosition) ?: All).let { notesAdapter.setFilter(it.filter) } + (TabPosition.entries.getOrNull(tabPosition) ?: All).let { notesAdapter.setFilter(it.filter) } } viewModel.updatedNote.observe(viewLifecycleOwner) { notesAdapter.updateNote(it) @@ -531,7 +531,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l noteId: String?, shouldShowKeyboard: Boolean = false, replyText: String? = null, - filter: FILTERS? = null, + filter: Filter? = null, isTappedFromPushNotification: Boolean = false, ) { if (noteId == null || activity == null || activity.isFinishing) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 2011743d63d6..9f8fea74238a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -37,12 +37,12 @@ import org.wordpress.android.models.Notification.PostNotification import org.wordpress.android.models.Notification.Unknown import org.wordpress.android.ui.comments.CommentUtils import org.wordpress.android.ui.notifications.NotificationsListViewModel.InlineActionEvent -import org.wordpress.android.ui.notifications.adapters.NotesAdapter.NoteViewHolder import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper import org.wordpress.android.util.GravatarUtils import org.wordpress.android.util.RtlUtils import org.wordpress.android.util.extensions.getColorFromAttribute +import org.wordpress.android.util.extensions.indexOrNull import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.image.ImageType import javax.inject.Inject @@ -51,10 +51,13 @@ import kotlin.math.roundToInt class NotesAdapter(context: Context, private val inlineActionEvents: MutableSharedFlow) : RecyclerView.Adapter() { private val coroutineScope = CoroutineScope(Dispatchers.IO) + private var reloadLocalNotesJob: Job? = null val filteredNotes = ArrayList() var onNoteClicked = { _: String -> } var onNotesLoaded = { _: Int -> } - var onScrolledToBottom = { _:Long -> } + var onScrolledToBottom = { _: Long -> } + var currentFilter = Filter.ALL + private set @Inject lateinit var imageManager: ImageManager @@ -72,29 +75,7 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar setHasStableIds(false) } - enum class FILTERS { - FILTER_ALL, - FILTER_COMMENT, - FILTER_FOLLOW, - FILTER_LIKE, - FILTER_UNREAD; - - override fun toString(): String { - return when (this) { - FILTER_ALL -> "all" - FILTER_COMMENT -> "comment" - FILTER_FOLLOW -> "follow" - FILTER_LIKE -> "like" - FILTER_UNREAD -> "unread" - } - } - } - - var currentFilter = FILTERS.FILTER_ALL - private set - private var reloadLocalNotesJob: Job? = null - - fun setFilter(newFilter: FILTERS) { + fun setFilter(newFilter: Filter) { currentFilter = newFilter } @@ -120,7 +101,11 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder = - NoteViewHolder(NotificationsListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + NoteViewHolder( + NotificationsListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + inlineActionEvents, + coroutineScope + ) override fun getItemCount(): Int = filteredNotes.size @@ -287,8 +272,7 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar fun reloadLocalNotes() { cancelReloadLocalNotes() reloadLocalNotesJob = coroutineScope.launch { - val notes = NotificationsTable.getLatestNotes() - addAll(notes) + addAll(NotificationsTable.getLatestNotes()) } } @@ -296,91 +280,102 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar * Update the note in the adapter and notify the change */ fun updateNote(note: Note) { - val notePosition = filteredNotes.indexOfFirst { it.id == note.id } - if (notePosition != -1) { + filteredNotes.indexOrNull { it.id == note.id }?.let { notePosition -> filteredNotes[notePosition] = note notifyItemChanged(notePosition) } } - inner class NoteViewHolder(val binding: NotificationsListItemBinding) : RecyclerView.ViewHolder(binding.root) { - fun bindInlineActionIconsForNote(note: Note) = Notification.from(note).let { notification -> - when (notification) { - Comment -> bindLikeCommentAction(note) - is PostNotification.NewPost -> bindLikePostAction(note) - is PostNotification -> bindShareAction(notification) - is Unknown -> { - binding.action.isVisible = false - } + companion object { + // Instead of building the filtered notes list dynamically, create it once and re-use it. + // Otherwise it's re-created so many times during layout. + @JvmStatic + fun buildFilteredNotesList( + notes: List, + filter: Filter + ): ArrayList = notes.filter { note -> + when (filter) { + Filter.ALL -> true + Filter.COMMENT -> note.isCommentType + Filter.FOLLOW -> note.isFollowType + Filter.UNREAD -> note.isUnread + Filter.LIKE -> note.isLikeType } - } + }.sortedByDescending { it.timestamp }.let { result -> ArrayList(result) } + } +} - private fun bindShareAction(notification: PostNotification) { - binding.action.setImageResource(R.drawable.block_share) - val color = binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) - ImageViewCompat.setImageTintList(binding.action, ColorStateList.valueOf(color)) - binding.action.isVisible = true - binding.action.setOnClickListener { - coroutineScope.launch { - inlineActionEvents.emit( - InlineActionEvent.SharePostButtonTapped(notification) - ) - } +class NoteViewHolder( + val binding: NotificationsListItemBinding, + private val inlineActionEvents: MutableSharedFlow, + private val coroutineScope: CoroutineScope +) : RecyclerView.ViewHolder(binding.root) { + fun bindInlineActionIconsForNote(note: Note) = Notification.from(note).let { notification -> + when (notification) { + Comment -> bindLikeCommentAction(note) + is PostNotification.NewPost -> bindLikePostAction(note) + is PostNotification -> bindShareAction(notification) + is Unknown -> { + binding.action.isVisible = false } } + } - private fun bindLikePostAction(note: Note) { - if (note.canLikePost().not()) return - setupLikeIcon(note.hasLikedPost()) - binding.action.setOnClickListener { - val liked = note.hasLikedPost().not() - setupLikeIcon(liked) - coroutineScope.launch { - inlineActionEvents.emit( - InlineActionEvent.LikePostButtonTapped(note, liked) - ) - } + private fun bindShareAction(notification: PostNotification) { + binding.action.setImageResource(R.drawable.block_share) + val color = binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) + ImageViewCompat.setImageTintList(binding.action, ColorStateList.valueOf(color)) + binding.action.isVisible = true + binding.action.setOnClickListener { + coroutineScope.launch { + inlineActionEvents.emit( + InlineActionEvent.SharePostButtonTapped(notification) + ) } } + } - private fun bindLikeCommentAction(note: Note) { - if (note.canLikeComment().not()) return - setupLikeIcon(note.hasLikedComment()) - binding.action.setOnClickListener { - val liked = note.hasLikedComment().not() - setupLikeIcon(liked) - coroutineScope.launch { - inlineActionEvents.emit( - InlineActionEvent.LikeCommentButtonTapped(note, liked) - ) - } + private fun bindLikePostAction(note: Note) { + if (note.canLikePost().not()) return + setupLikeIcon(note.hasLikedPost()) + binding.action.setOnClickListener { + val liked = note.hasLikedPost().not() + setupLikeIcon(liked) + coroutineScope.launch { + inlineActionEvents.emit( + InlineActionEvent.LikePostButtonTapped(note, liked) + ) } } + } - private fun setupLikeIcon(liked: Boolean) { - binding.action.isVisible = true - binding.action.setImageResource(if (liked) R.drawable.star_filled else R.drawable.star_empty) - val color = if (liked) binding.root.context.getColor(R.color.inline_action_filled) - else binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) - ImageViewCompat.setImageTintList(binding.action, ColorStateList.valueOf(color)) + private fun bindLikeCommentAction(note: Note) { + if (note.canLikeComment().not()) return + setupLikeIcon(note.hasLikedComment()) + binding.action.setOnClickListener { + val liked = note.hasLikedComment().not() + setupLikeIcon(liked) + coroutineScope.launch { + inlineActionEvents.emit( + InlineActionEvent.LikeCommentButtonTapped(note, liked) + ) + } } } - companion object { - // Instead of building the filtered notes list dynamically, create it once and re-use it. - // Otherwise it's re-created so many times during layout. - @JvmStatic - fun buildFilteredNotesList( - notes: List, - filter: FILTERS - ): ArrayList = notes.filter { note -> - when (filter) { - FILTERS.FILTER_ALL -> true - FILTERS.FILTER_COMMENT -> note.isCommentType - FILTERS.FILTER_FOLLOW -> note.isFollowType - FILTERS.FILTER_UNREAD -> note.isUnread - FILTERS.FILTER_LIKE -> note.isLikeType - } - }.sortedByDescending { it.timestamp }.let { result -> ArrayList(result) } + private fun setupLikeIcon(liked: Boolean) { + binding.action.isVisible = true + binding.action.setImageResource(if (liked) R.drawable.star_filled else R.drawable.star_empty) + val color = if (liked) binding.root.context.getColor(R.color.inline_action_filled) + else binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) + ImageViewCompat.setImageTintList(binding.action, ColorStateList.valueOf(color)) } } + +enum class Filter(val value: String) { + ALL("all"), + COMMENT("comment"), + FOLLOW("follow"), + LIKE("like"), + UNREAD("unread"); +} From 62591c6a3387a8242726f57a3917a81e14f6f5b7 Mon Sep 17 00:00:00 2001 From: Danilo Ercoli Date: Mon, 19 Feb 2024 12:21:33 +0100 Subject: [PATCH 17/38] Remove duplicates from the server blogs list if local and remote doesn't match. Remove duplicates from the server blogs list if local and remote doesn't match. This is required because under obscure circumstances the server can return duplicates. We could have modified the function isSameList to eliminate the length check, but it's better to keep it separate since we aim to remove this check as soon as possible. --- .../services/update/ReaderUpdateLogic.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java index a9af93b20e15..d7d6febd20dd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java @@ -14,6 +14,7 @@ import org.wordpress.android.datasets.ReaderPostTable; import org.wordpress.android.datasets.ReaderTagTable; import org.wordpress.android.fluxc.store.AccountStore; +import org.wordpress.android.models.ReaderBlog; import org.wordpress.android.models.ReaderBlogList; import org.wordpress.android.models.ReaderTag; import org.wordpress.android.models.ReaderTagList; @@ -321,7 +322,13 @@ private void handleFollowedBlogsResponse(final JSONObject jsonObject) { public void run() { ReaderBlogList serverBlogs = ReaderBlogList.fromJson(jsonObject); ReaderBlogList localBlogs = ReaderBlogTable.getFollowedBlogs(); - + // Remove duplicates from the server blogs list only if local and remote lists don't match. + if (serverBlogs.size() != localBlogs.size()) { + // This is required because under rare circumstances the server can return duplicates. + // We could have modified the function isSameList to eliminate the length check, + // but it's better to keep it separate since we aim to remove this check as soon as possible. + removeDuplicateFromServerResponse(serverBlogs); + } if (!localBlogs.isSameList(serverBlogs)) { // always update the list of followed blogs if there are *any* changes between // server and local (including subscription count, description, etc.) @@ -338,6 +345,20 @@ public void run() { taskCompleted(UpdateTask.FOLLOWED_BLOGS); } + /* This method remove duplicate ReaderBlog from list. */ + private void removeDuplicateFromServerResponse(ReaderBlogList serverBlogs) { + for (int i = 0; i < serverBlogs.size(); i++) { + ReaderBlog outer = serverBlogs.get(i); + for (int j = serverBlogs.size() - 1; j > i; j--) { + ReaderBlog inner = serverBlogs.get(j); + if (outer.blogId == inner.blogId) { + // If the 'id' property is the same, + // remove the later object to avoid duplicates + serverBlogs.remove(j); + } + } + } + } }.start(); } } From 3d8433659a3a046a167cb24231ec72b2a58ee4c2 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 19:39:54 +0800 Subject: [PATCH 18/38] Extract NoteViewHolder from NoteAdapter --- .../notifications/adapters/NoteViewHolder.kt | 263 ++++++++++++++++++ .../ui/notifications/adapters/NotesAdapter.kt | 252 +---------------- 2 files changed, 272 insertions(+), 243 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteViewHolder.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteViewHolder.kt new file mode 100644 index 000000000000..5e7e37ce3c82 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteViewHolder.kt @@ -0,0 +1,263 @@ +package org.wordpress.android.ui.notifications.adapters + +import android.content.res.ColorStateList +import android.text.Spanned +import android.text.TextUtils +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.core.text.BidiFormatter +import androidx.core.view.ViewCompat +import androidx.core.view.isVisible +import androidx.core.widget.ImageViewCompat +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import org.wordpress.android.R +import org.wordpress.android.databinding.NotificationsListItemBinding +import org.wordpress.android.models.Note +import org.wordpress.android.models.Notification +import org.wordpress.android.ui.comments.CommentUtils +import org.wordpress.android.ui.notifications.NotificationsListViewModel +import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan +import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper +import org.wordpress.android.util.GravatarUtils +import org.wordpress.android.util.RtlUtils +import org.wordpress.android.util.extensions.getColorFromAttribute +import org.wordpress.android.util.image.ImageManager +import org.wordpress.android.util.image.ImageType +import javax.inject.Inject +import kotlin.math.roundToInt + +class NoteViewHolder( + val binding: NotificationsListItemBinding, + private val inlineActionEvents: MutableSharedFlow, + private val coroutineScope: CoroutineScope +) : RecyclerView.ViewHolder(binding.root) { + @Inject + lateinit var notificationsUtilsWrapper: NotificationsUtilsWrapper + @Inject + lateinit var imageManager: ImageManager + fun bindTimeGroupHeader(note: Note, previousNote: Note?, position: Int) { + // Display time group header + timeGroupHeaderText(note, previousNote)?.let { timeGroupText -> + with(binding.headerText) { + visibility = View.VISIBLE + setText(timeGroupText) + } + } ?: run { + binding.headerText.visibility = View.GONE + } + + // handle the margin top for the header + val headerMarginTop: Int + val context = itemView.context + headerMarginTop = if (position == 0) { + context.resources + .getDimensionPixelSize(R.dimen.notifications_header_margin_top_position_0) + } else { + context.resources + .getDimensionPixelSize(R.dimen.notifications_header_margin_top_position_n) + } + val layoutParams = binding.headerText.layoutParams as ViewGroup.MarginLayoutParams + layoutParams.topMargin = headerMarginTop + binding.headerText.layoutParams = layoutParams + } + + fun bindInlineActions(note: Note) = Notification.from(note).let { notification -> + when (notification) { + Notification.Comment -> bindLikeCommentAction(note) + is Notification.PostNotification.NewPost -> bindLikePostAction(note) + is Notification.PostNotification -> bindShareAction(notification) + is Notification.Unknown -> { + binding.action.isVisible = false + } + } + } + + private fun bindShareAction(notification: Notification.PostNotification) { + binding.action.setImageResource(R.drawable.block_share) + val color = binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) + ImageViewCompat.setImageTintList(binding.action, ColorStateList.valueOf(color)) + binding.action.isVisible = true + binding.action.setOnClickListener { + coroutineScope.launch { + inlineActionEvents.emit( + NotificationsListViewModel.InlineActionEvent.SharePostButtonTapped(notification) + ) + } + } + } + + private fun bindLikePostAction(note: Note) { + if (note.canLikePost().not()) return + setupLikeIcon(note.hasLikedPost()) + binding.action.setOnClickListener { + val liked = note.hasLikedPost().not() + setupLikeIcon(liked) + coroutineScope.launch { + inlineActionEvents.emit( + NotificationsListViewModel.InlineActionEvent.LikePostButtonTapped(note, liked) + ) + } + } + } + + private fun bindLikeCommentAction(note: Note) { + if (note.canLikeComment().not()) return + setupLikeIcon(note.hasLikedComment()) + binding.action.setOnClickListener { + val liked = note.hasLikedComment().not() + setupLikeIcon(liked) + coroutineScope.launch { + inlineActionEvents.emit( + NotificationsListViewModel.InlineActionEvent.LikeCommentButtonTapped( + note, + liked + ) + ) + } + } + } + + private fun setupLikeIcon(liked: Boolean) { + binding.action.isVisible = true + binding.action.setImageResource(if (liked) R.drawable.star_filled else R.drawable.star_empty) + val color = if (liked) binding.root.context.getColor(R.color.inline_action_filled) + else binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) + ImageViewCompat.setImageTintList(binding.action, ColorStateList.valueOf(color)) + } + + @StringRes + private fun timeGroupHeaderText(note: Note, previousNote: Note?) = + previousNote?.timeGroup.let { previousTimeGroup -> + val timeGroup = note.timeGroup + if (previousTimeGroup?.let { it == timeGroup } == true) { + // If the previous time group exists and is the same, we don't need a new one + null + } else { + // Otherwise, we create a new one + when (timeGroup) { + Note.NoteTimeGroup.GROUP_TODAY -> R.string.stats_timeframe_today + Note.NoteTimeGroup.GROUP_YESTERDAY -> R.string.stats_timeframe_yesterday + Note.NoteTimeGroup.GROUP_OLDER_TWO_DAYS -> R.string.older_two_days + Note.NoteTimeGroup.GROUP_OLDER_WEEK -> R.string.older_last_week + Note.NoteTimeGroup.GROUP_OLDER_MONTH -> R.string.older_month + } + } + } + + fun bindSubject(note: Note) { + // Subject is stored in db as html to preserve text formatting + var noteSubjectSpanned: Spanned = note.getFormattedSubject(notificationsUtilsWrapper) + // Trim the '\n\n' added by HtmlCompat.fromHtml(...) + noteSubjectSpanned = noteSubjectSpanned.subSequence( + 0, + TextUtils.getTrimmedLength(noteSubjectSpanned) + ) as Spanned + val spans = noteSubjectSpanned.getSpans( + 0, + noteSubjectSpanned.length, + NoteBlockClickableSpan::class.java + ) + for (span in spans) { + span.enableColors(itemView.context) + } + binding.noteSubject.text = noteSubjectSpanned + } + + fun bindSubjectNoticon(note: Note) { + val noteSubjectNoticon = note.commentSubjectNoticon + if (!TextUtils.isEmpty(noteSubjectNoticon)) { + val parent = binding.noteSubject.parent + // Fix position of the subject noticon in the RtL mode + if (parent is ViewGroup) { + val textDirection = if (BidiFormatter.getInstance() + .isRtl(binding.noteSubject.text) + ) ViewCompat.LAYOUT_DIRECTION_RTL else ViewCompat.LAYOUT_DIRECTION_LTR + ViewCompat.setLayoutDirection(parent, textDirection) + } + // mirror noticon in the rtl mode + if (RtlUtils.isRtl(itemView.context)) { + binding.noteSubjectNoticon.scaleX = -1f + } + val textIndentSize = itemView.context.resources + .getDimensionPixelSize(R.dimen.notifications_text_indent_sz) + CommentUtils.indentTextViewFirstLine(binding.noteSubject, textIndentSize) + binding.noteSubjectNoticon.text = noteSubjectNoticon + binding.noteSubjectNoticon.visibility = View.VISIBLE + } else { + binding.noteSubjectNoticon.visibility = View.GONE + } + } + + fun bindContent(note: Note) { + val noteSnippet = note.commentSubject + if (!TextUtils.isEmpty(noteSnippet)) { + handleMaxLines(binding.noteSubject, binding.noteDetail) + binding.noteDetail.text = noteSnippet + binding.noteDetail.visibility = View.VISIBLE + } else { + binding.noteDetail.visibility = View.GONE + } + } + + private fun handleMaxLines(subject: TextView, detail: TextView) { + subject.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + subject.viewTreeObserver.removeOnPreDrawListener(this) + if (subject.lineCount == 2) { + detail.maxLines = 1 + } else { + detail.maxLines = 2 + } + return false + } + }) + } + + fun bindAvatars(note: Note) { + if (note.shouldShowMultipleAvatars() && note.iconURLs != null && note.iconURLs!!.size > 1) { + val avatars = note.iconURLs!!.toList() + if (avatars.size == 2) { + binding.noteAvatar.visibility = View.INVISIBLE + binding.twoAvatarsView.root.visibility = View.VISIBLE + binding.threeAvatarsView.root.visibility = View.INVISIBLE + loadAvatar(binding.twoAvatarsView.twoAvatars1, avatars[1]) + loadAvatar(binding.twoAvatarsView.twoAvatars2, avatars[0]) + } else { // size > 3 + binding.noteAvatar.visibility = View.INVISIBLE + binding.twoAvatarsView.root.visibility = View.INVISIBLE + binding.threeAvatarsView.root.visibility = View.VISIBLE + loadAvatar(binding.threeAvatarsView.threeAvatars1, avatars[2]) + loadAvatar(binding.threeAvatarsView.threeAvatars2, avatars[1]) + loadAvatar(binding.threeAvatarsView.threeAvatars3, avatars[0]) + } + } else { // single avatar + binding.noteAvatar.visibility = View.VISIBLE + binding.twoAvatarsView.root.visibility = View.INVISIBLE + binding.threeAvatarsView.root.visibility = View.INVISIBLE + loadAvatar(binding.noteAvatar, note.iconURL) + } + } + + private fun loadAvatar(imageView: ImageView, avatarUrl: String) { + val avatarSize = imageView.context.resources.getDimension(R.dimen.notifications_avatar_sz).roundToInt() + val url = GravatarUtils.fixGravatarUrl(avatarUrl, avatarSize) + imageManager.loadIntoCircle(imageView, ImageType.AVATAR_WITH_BACKGROUND, url) + } + + private fun Note.shouldShowMultipleAvatars() = isFollowType || isLikeType || isCommentLikeType + + fun bindOthers(note: Note, onNoteClicked: (String) -> Unit) { + binding.noteContentContainer.setOnClickListener { onNoteClicked(note.id) } + binding.notificationUnread.isVisible = note.isUnread + } + + private val Note.timeGroup + get() = Note.getTimeGroupForTimestamp(timestamp) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 9f8fea74238a..26488a9f5b1f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -1,21 +1,8 @@ package org.wordpress.android.ui.notifications.adapters import android.content.Context -import android.content.res.ColorStateList -import android.text.Spanned -import android.text.TextUtils import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.view.ViewGroup.MarginLayoutParams -import android.view.ViewTreeObserver -import android.widget.ImageView -import android.widget.TextView -import androidx.annotation.StringRes -import androidx.core.text.BidiFormatter -import androidx.core.view.ViewCompat -import androidx.core.view.isVisible -import androidx.core.widget.ImageViewCompat import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil.ItemCallback import androidx.recyclerview.widget.RecyclerView @@ -25,28 +12,12 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.NotificationsListItemBinding import org.wordpress.android.datasets.NotificationsTable import org.wordpress.android.models.Note -import org.wordpress.android.models.Note.NoteTimeGroup -import org.wordpress.android.models.Notification -import org.wordpress.android.models.Notification.Comment -import org.wordpress.android.models.Notification.PostNotification -import org.wordpress.android.models.Notification.Unknown -import org.wordpress.android.ui.comments.CommentUtils import org.wordpress.android.ui.notifications.NotificationsListViewModel.InlineActionEvent -import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan -import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper -import org.wordpress.android.util.GravatarUtils -import org.wordpress.android.util.RtlUtils -import org.wordpress.android.util.extensions.getColorFromAttribute import org.wordpress.android.util.extensions.indexOrNull -import org.wordpress.android.util.image.ImageManager -import org.wordpress.android.util.image.ImageType -import javax.inject.Inject -import kotlin.math.roundToInt class NotesAdapter(context: Context, private val inlineActionEvents: MutableSharedFlow) : RecyclerView.Adapter() { @@ -59,12 +30,6 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar var currentFilter = Filter.ALL private set - @Inject - lateinit var imageManager: ImageManager - - @Inject - lateinit var notificationsUtilsWrapper: NotificationsUtilsWrapper - init { (context.applicationContext as WordPress).component().inject(this) @@ -84,7 +49,7 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar */ fun addAll(notes: List) = coroutineScope.launch { val newNotes = buildFilteredNotesList(notes, currentFilter) - val differ = AsyncListDiffer(this@NotesAdapter, object: ItemCallback(){ + val differ = AsyncListDiffer(this@NotesAdapter, object : ItemCallback() { override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean = oldItem.id == newItem.id @@ -109,157 +74,25 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar override fun getItemCount(): Int = filteredNotes.size - private val Note.timeGroup - get() = Note.getTimeGroupForTimestamp(timestamp) - - @StringRes - private fun timeGroupHeaderText(note: Note, previousNote: Note?) = - previousNote?.timeGroup.let { previousTimeGroup -> - val timeGroup = note.timeGroup - if (previousTimeGroup?.let { it == timeGroup } == true) { - // If the previous time group exists and is the same, we don't need a new one - null - } else { - // Otherwise, we create a new one - when (timeGroup) { - NoteTimeGroup.GROUP_TODAY -> R.string.stats_timeframe_today - NoteTimeGroup.GROUP_YESTERDAY -> R.string.stats_timeframe_yesterday - NoteTimeGroup.GROUP_OLDER_TWO_DAYS -> R.string.older_two_days - NoteTimeGroup.GROUP_OLDER_WEEK -> R.string.older_last_week - NoteTimeGroup.GROUP_OLDER_MONTH -> R.string.older_month - } - } - } - @Suppress("CyclomaticComplexMethod", "LongMethod") override fun onBindViewHolder(noteViewHolder: NoteViewHolder, position: Int) { val note = filteredNotes.getOrNull(position) ?: return val previousNote = filteredNotes.getOrNull(position - 1) - noteViewHolder.binding.noteContentContainer.setOnClickListener { onNoteClicked(note.id) } - // Display time group header - timeGroupHeaderText(note, previousNote)?.let { timeGroupText -> - with(noteViewHolder.binding.headerText) { - visibility = View.VISIBLE - setText(timeGroupText) - } - } ?: run { - noteViewHolder.binding.headerText.visibility = View.GONE - } - // Subject is stored in db as html to preserve text formatting - var noteSubjectSpanned: Spanned = note.getFormattedSubject(notificationsUtilsWrapper) - // Trim the '\n\n' added by HtmlCompat.fromHtml(...) - noteSubjectSpanned = noteSubjectSpanned.subSequence( - 0, - TextUtils.getTrimmedLength(noteSubjectSpanned) - ) as Spanned - val spans = noteSubjectSpanned.getSpans( - 0, - noteSubjectSpanned.length, - NoteBlockClickableSpan::class.java - ) - for (span in spans) { - span.enableColors(noteViewHolder.itemView.context) - } - noteViewHolder.binding.noteSubject.text = noteSubjectSpanned - val noteSubjectNoticon = note.commentSubjectNoticon - if (!TextUtils.isEmpty(noteSubjectNoticon)) { - val parent = noteViewHolder.binding.noteSubject.parent - // Fix position of the subject noticon in the RtL mode - if (parent is ViewGroup) { - val textDirection = if (BidiFormatter.getInstance() - .isRtl(noteViewHolder.binding.noteSubject.text) - ) ViewCompat.LAYOUT_DIRECTION_RTL else ViewCompat.LAYOUT_DIRECTION_LTR - ViewCompat.setLayoutDirection(parent, textDirection) - } - // mirror noticon in the rtl mode - if (RtlUtils.isRtl(noteViewHolder.itemView.context)) { - noteViewHolder.binding.noteSubjectNoticon.scaleX = -1f - } - val textIndentSize = noteViewHolder.itemView.context.resources - .getDimensionPixelSize(R.dimen.notifications_text_indent_sz) - CommentUtils.indentTextViewFirstLine(noteViewHolder.binding.noteSubject, textIndentSize) - noteViewHolder.binding.noteSubjectNoticon.text = noteSubjectNoticon - noteViewHolder.binding.noteSubjectNoticon.visibility = View.VISIBLE - } else { - noteViewHolder.binding.noteSubjectNoticon.visibility = View.GONE - } - val noteSnippet = note.commentSubject - if (!TextUtils.isEmpty(noteSnippet)) { - handleMaxLines(noteViewHolder.binding.noteSubject, noteViewHolder.binding.noteDetail) - noteViewHolder.binding.noteDetail.text = noteSnippet - noteViewHolder.binding.noteDetail.visibility = View.VISIBLE - } else { - noteViewHolder.binding.noteDetail.visibility = View.GONE - } - noteViewHolder.loadAvatars(note) - noteViewHolder.bindInlineActionIconsForNote(note) - noteViewHolder.binding.notificationUnread.isVisible = note.isUnread + noteViewHolder.bindTimeGroupHeader(note, previousNote, position) + noteViewHolder.bindSubject(note) + noteViewHolder.bindSubjectNoticon(note) + noteViewHolder.bindContent(note) + noteViewHolder.bindAvatars(note) + noteViewHolder.bindInlineActions(note) + noteViewHolder.bindOthers(note, onNoteClicked) + // request to load more comments when we near the end if (position >= itemCount - 1) { onScrolledToBottom(note.timestamp) } - val headerMarginTop: Int - val context = noteViewHolder.itemView.context - headerMarginTop = if (position == 0) { - context.resources - .getDimensionPixelSize(R.dimen.notifications_header_margin_top_position_0) - } else { - context.resources - .getDimensionPixelSize(R.dimen.notifications_header_margin_top_position_n) - } - val layoutParams = noteViewHolder.binding.headerText.layoutParams as MarginLayoutParams - layoutParams.topMargin = headerMarginTop - noteViewHolder.binding.headerText.layoutParams = layoutParams - } - - private fun NoteViewHolder.loadAvatars(note: Note) { - if (note.shouldShowMultipleAvatars() && note.iconURLs != null && note.iconURLs!!.size > 1) { - val avatars = note.iconURLs!!.toList() - if (avatars.size == 2) { - binding.noteAvatar.visibility = View.INVISIBLE - binding.twoAvatarsView.root.visibility = View.VISIBLE - binding.threeAvatarsView.root.visibility = View.INVISIBLE - loadAvatar(binding.twoAvatarsView.twoAvatars1, avatars[1]) - loadAvatar(binding.twoAvatarsView.twoAvatars2, avatars[0]) - } else { // size > 3 - binding.noteAvatar.visibility = View.INVISIBLE - binding.twoAvatarsView.root.visibility = View.INVISIBLE - binding.threeAvatarsView.root.visibility = View.VISIBLE - loadAvatar(binding.threeAvatarsView.threeAvatars1, avatars[2]) - loadAvatar(binding.threeAvatarsView.threeAvatars2, avatars[1]) - loadAvatar(binding.threeAvatarsView.threeAvatars3, avatars[0]) - } - } else { // single avatar - binding.noteAvatar.visibility = View.VISIBLE - binding.twoAvatarsView.root.visibility = View.INVISIBLE - binding.threeAvatarsView.root.visibility = View.INVISIBLE - loadAvatar(binding.noteAvatar, note.iconURL) - } - } - - private fun loadAvatar(imageView: ImageView, avatarUrl: String) { - val avatarSize = imageView.context.resources.getDimension(R.dimen.notifications_avatar_sz).roundToInt() - val url = GravatarUtils.fixGravatarUrl(avatarUrl, avatarSize) - imageManager.loadIntoCircle(imageView, ImageType.AVATAR_WITH_BACKGROUND, url) - } - - private fun Note.shouldShowMultipleAvatars() = isFollowType || isLikeType || isCommentLikeType - - private fun handleMaxLines(subject: TextView, detail: TextView) { - subject.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { - override fun onPreDraw(): Boolean { - subject.viewTreeObserver.removeOnPreDrawListener(this) - if (subject.lineCount == 2) { - detail.maxLines = 1 - } else { - detail.maxLines = 2 - } - return false - } - }) } fun cancelReloadLocalNotes() { @@ -305,73 +138,6 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar } } -class NoteViewHolder( - val binding: NotificationsListItemBinding, - private val inlineActionEvents: MutableSharedFlow, - private val coroutineScope: CoroutineScope -) : RecyclerView.ViewHolder(binding.root) { - fun bindInlineActionIconsForNote(note: Note) = Notification.from(note).let { notification -> - when (notification) { - Comment -> bindLikeCommentAction(note) - is PostNotification.NewPost -> bindLikePostAction(note) - is PostNotification -> bindShareAction(notification) - is Unknown -> { - binding.action.isVisible = false - } - } - } - - private fun bindShareAction(notification: PostNotification) { - binding.action.setImageResource(R.drawable.block_share) - val color = binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) - ImageViewCompat.setImageTintList(binding.action, ColorStateList.valueOf(color)) - binding.action.isVisible = true - binding.action.setOnClickListener { - coroutineScope.launch { - inlineActionEvents.emit( - InlineActionEvent.SharePostButtonTapped(notification) - ) - } - } - } - - private fun bindLikePostAction(note: Note) { - if (note.canLikePost().not()) return - setupLikeIcon(note.hasLikedPost()) - binding.action.setOnClickListener { - val liked = note.hasLikedPost().not() - setupLikeIcon(liked) - coroutineScope.launch { - inlineActionEvents.emit( - InlineActionEvent.LikePostButtonTapped(note, liked) - ) - } - } - } - - private fun bindLikeCommentAction(note: Note) { - if (note.canLikeComment().not()) return - setupLikeIcon(note.hasLikedComment()) - binding.action.setOnClickListener { - val liked = note.hasLikedComment().not() - setupLikeIcon(liked) - coroutineScope.launch { - inlineActionEvents.emit( - InlineActionEvent.LikeCommentButtonTapped(note, liked) - ) - } - } - } - - private fun setupLikeIcon(liked: Boolean) { - binding.action.isVisible = true - binding.action.setImageResource(if (liked) R.drawable.star_filled else R.drawable.star_empty) - val color = if (liked) binding.root.context.getColor(R.color.inline_action_filled) - else binding.root.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) - ImageViewCompat.setImageTintList(binding.action, ColorStateList.valueOf(color)) - } -} - enum class Filter(val value: String) { ALL("all"), COMMENT("comment"), From 728a6cdc429fa4590a29be650ea046c9955b9c82 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 19:52:34 +0800 Subject: [PATCH 19/38] Fix a DI issue --- .../java/org/wordpress/android/modules/AppComponent.java | 3 +++ .../android/ui/notifications/adapters/NoteViewHolder.kt | 8 +++++++- .../android/ui/notifications/adapters/NotesAdapter.kt | 3 --- 3 files changed, 10 insertions(+), 4 deletions(-) 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 993703e791c2..76cc273160c2 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -61,6 +61,7 @@ import org.wordpress.android.ui.notifications.NotificationsDetailListFragment; import org.wordpress.android.ui.notifications.NotificationsListFragmentPage; import org.wordpress.android.ui.notifications.adapters.NotesAdapter; +import org.wordpress.android.ui.notifications.adapters.NoteViewHolder; import org.wordpress.android.ui.notifications.receivers.NotificationsPendingDraftsReceiver; import org.wordpress.android.ui.pages.PageListFragment; import org.wordpress.android.ui.pages.PageParentFragment; @@ -343,6 +344,8 @@ public interface AppComponent { void inject(NotesAdapter object); + void inject(NoteViewHolder object); + void inject(ThemeBrowserFragment object); void inject(SelectCategoriesActivity object); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteViewHolder.kt index 5e7e37ce3c82..d0a6f239f6d5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NoteViewHolder.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import org.wordpress.android.R +import org.wordpress.android.WordPress import org.wordpress.android.databinding.NotificationsListItemBinding import org.wordpress.android.models.Note import org.wordpress.android.models.Notification @@ -34,7 +35,7 @@ import javax.inject.Inject import kotlin.math.roundToInt class NoteViewHolder( - val binding: NotificationsListItemBinding, + private val binding: NotificationsListItemBinding, private val inlineActionEvents: MutableSharedFlow, private val coroutineScope: CoroutineScope ) : RecyclerView.ViewHolder(binding.root) { @@ -42,6 +43,11 @@ class NoteViewHolder( lateinit var notificationsUtilsWrapper: NotificationsUtilsWrapper @Inject lateinit var imageManager: ImageManager + + init { + (itemView.context.applicationContext as WordPress).component().inject(this) + } + fun bindTimeGroupHeader(note: Note, previousNote: Note?, position: Int) { // Display time group header timeGroupHeaderText(note, previousNote)?.let { timeGroupText -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 26488a9f5b1f..a532c5e97457 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -74,12 +74,10 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar override fun getItemCount(): Int = filteredNotes.size - override fun onBindViewHolder(noteViewHolder: NoteViewHolder, position: Int) { val note = filteredNotes.getOrNull(position) ?: return val previousNote = filteredNotes.getOrNull(position - 1) - noteViewHolder.bindTimeGroupHeader(note, previousNote, position) noteViewHolder.bindSubject(note) noteViewHolder.bindSubjectNoticon(note) @@ -88,7 +86,6 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar noteViewHolder.bindInlineActions(note) noteViewHolder.bindOthers(note, onNoteClicked) - // request to load more comments when we near the end if (position >= itemCount - 1) { onScrolledToBottom(note.timestamp) From b83c0b6920a051b9ceb55de0f8d0562f517805f8 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 20:57:39 +0800 Subject: [PATCH 20/38] Fix the IndexOutOfBoundException --- .../ui/notifications/adapters/NotesAdapter.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index a532c5e97457..14c365d1ed56 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -48,18 +48,19 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar * Add notes to the adapter and notify the change */ fun addAll(notes: List) = coroutineScope.launch { - val newNotes = buildFilteredNotesList(notes, currentFilter) - val differ = AsyncListDiffer(this@NotesAdapter, object : ItemCallback() { - override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean = - oldItem.id == newItem.id + withContext(Dispatchers.Main) { + val newNotes = buildFilteredNotesList(notes, currentFilter) + val differ = AsyncListDiffer(this@NotesAdapter, object : ItemCallback() { + override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean = + oldItem.id == newItem.id - override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean = - oldItem.json.toString() == newItem.json.toString() - }) + override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean = + oldItem.json.toString() == newItem.json.toString() + }) + + filteredNotes.clear() + filteredNotes.addAll(newNotes) - filteredNotes.clear() - filteredNotes.addAll(newNotes) - withContext(Dispatchers.Main) { differ.submitList(newNotes) onNotesLoaded(itemCount) } From 3e483828c2508f978275d4632815938bc6a143a2 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 21:00:20 +0800 Subject: [PATCH 21/38] Add a comment --- .../wordpress/android/ui/notifications/adapters/NotesAdapter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 14c365d1ed56..554c0e125bc8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -49,6 +49,7 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar */ fun addAll(notes: List) = coroutineScope.launch { withContext(Dispatchers.Main) { + // make sure that the differ should be in the main thread, otherwise it will throw an IndexOutOfBoundsException val newNotes = buildFilteredNotesList(notes, currentFilter) val differ = AsyncListDiffer(this@NotesAdapter, object : ItemCallback() { override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean = From 1f716c67a1e96d8a2908106b5a751091b7d2715b Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 21:07:30 +0800 Subject: [PATCH 22/38] Fix a lint issue --- .../android/ui/notifications/adapters/NotesAdapter.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 554c0e125bc8..bf73664b5f98 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -49,7 +49,8 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar */ fun addAll(notes: List) = coroutineScope.launch { withContext(Dispatchers.Main) { - // make sure that the differ should be in the main thread, otherwise it will throw an IndexOutOfBoundsException + // make sure that the differ should be in the main thread, + // otherwise it will throw an IndexOutOfBoundsException val newNotes = buildFilteredNotesList(notes, currentFilter) val differ = AsyncListDiffer(this@NotesAdapter, object : ItemCallback() { override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean = From 08588e4554f855249f38d3b85dd7c5687d467e7c Mon Sep 17 00:00:00 2001 From: Danilo Ercoli Date: Mon, 19 Feb 2024 16:07:15 +0100 Subject: [PATCH 23/38] Rename 'removeDuplicateBlogs' to something more generic and move it outside of the Thread object --- .../services/update/ReaderUpdateLogic.java | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java index d7d6febd20dd..cde09194380f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/update/ReaderUpdateLogic.java @@ -2,6 +2,8 @@ import android.content.Context; +import androidx.annotation.NonNull; + import com.android.volley.VolleyError; import com.wordpress.rest.RestRequest; @@ -322,13 +324,12 @@ private void handleFollowedBlogsResponse(final JSONObject jsonObject) { public void run() { ReaderBlogList serverBlogs = ReaderBlogList.fromJson(jsonObject); ReaderBlogList localBlogs = ReaderBlogTable.getFollowedBlogs(); - // Remove duplicates from the server blogs list only if local and remote lists don't match. - if (serverBlogs.size() != localBlogs.size()) { - // This is required because under rare circumstances the server can return duplicates. - // We could have modified the function isSameList to eliminate the length check, - // but it's better to keep it separate since we aim to remove this check as soon as possible. - removeDuplicateFromServerResponse(serverBlogs); - } + + // This is required because under rare circumstances the server can return duplicates. + // We could have modified the function isSameList to eliminate the length check, + // but it's better to keep it separate since we aim to remove this check as soon as possible. + removeDuplicateBlogs(serverBlogs); + if (!localBlogs.isSameList(serverBlogs)) { // always update the list of followed blogs if there are *any* changes between // server and local (including subscription count, description, etc.) @@ -345,20 +346,26 @@ public void run() { taskCompleted(UpdateTask.FOLLOWED_BLOGS); } - /* This method remove duplicate ReaderBlog from list. */ - private void removeDuplicateFromServerResponse(ReaderBlogList serverBlogs) { - for (int i = 0; i < serverBlogs.size(); i++) { - ReaderBlog outer = serverBlogs.get(i); - for (int j = serverBlogs.size() - 1; j > i; j--) { - ReaderBlog inner = serverBlogs.get(j); - if (outer.blogId == inner.blogId) { - // If the 'id' property is the same, - // remove the later object to avoid duplicates - serverBlogs.remove(j); - } - } + }.start(); + } + + /** + * Remove duplicates from the input list. + * Note that this method modifies the input list. + * + * @param blogList The list of blogs to remove duplicates from. + */ + private void removeDuplicateBlogs(@NonNull ReaderBlogList blogList) { + for (int i = 0; i < blogList.size(); i++) { + ReaderBlog outer = blogList.get(i); + for (int j = blogList.size() - 1; j > i; j--) { + ReaderBlog inner = blogList.get(j); + if (outer.blogId == inner.blogId) { + // If the 'id' property is the same, + // remove the later object to avoid duplicates + blogList.remove(j); } } - }.start(); + } } } From 12717f8193f4c1ffffd039f2cb339df433dd29c7 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 23:17:03 +0800 Subject: [PATCH 24/38] Fix the IndexOutOfBoundsException --- .../NotificationsListFragmentPage.kt | 27 ++++++++++++++++++- .../ui/notifications/adapters/NotesAdapter.kt | 2 -- 2 files changed, 26 insertions(+), 3 deletions(-) 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 index d370fbf4284d..bd4817a88b56 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -1,9 +1,11 @@ package org.wordpress.android.ui.notifications import android.app.Activity +import android.content.Context import android.content.Intent import android.os.Bundle import android.text.TextUtils +import android.util.AttributeSet import android.view.View import android.view.animation.Animation import android.view.animation.Animation.AnimationListener @@ -72,6 +74,7 @@ import org.wordpress.android.util.helpers.SwipeToRefreshHelper import org.wordpress.android.widgets.AppRatingDialog.incrementInteractions import javax.inject.Inject + @AndroidEntryPoint class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_list_fragment_page), OnScrollToTopListener { @@ -129,7 +132,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l .launchIn(viewLifecycleOwner.lifecycleScope) } binding = NotificationsListFragmentPageBinding.bind(view).apply { - notificationsList.layoutManager = LinearLayoutManager(activity) + notificationsList.layoutManager = LinearLayoutManagerWrapper(activity) notificationsList.adapter = notesAdapter swipeToRefreshHelper = WPSwipeToRefreshHelper.buildSwipeToRefreshHelper(notificationsRefresh) { hideNewNotificationsBar() @@ -554,4 +557,26 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL) } } + + /** + * LinearLayoutManagerWrapper is a workaround for a bug in RecyclerView that causes IndexOutOfBoundsException + * @see [link](https://stackoverflow.com/questions/31759171/recyclerview-and-java-lang-indexoutofboundsexception-inconsistency-detected-in) + */ + internal class LinearLayoutManagerWrapper : LinearLayoutManager { + constructor(context: Context) : super(context) + constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super( + context, + orientation, + reverseLayout + ) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super( + context, + attrs, + defStyleAttr, + defStyleRes + ) + + override fun supportsPredictiveItemAnimations(): Boolean = false + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index bf73664b5f98..14c365d1ed56 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -49,8 +49,6 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar */ fun addAll(notes: List) = coroutineScope.launch { withContext(Dispatchers.Main) { - // make sure that the differ should be in the main thread, - // otherwise it will throw an IndexOutOfBoundsException val newNotes = buildFilteredNotesList(notes, currentFilter) val differ = AsyncListDiffer(this@NotesAdapter, object : ItemCallback() { override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean = From edac734c6caf5647f1ac98784cf1191663aa0b07 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Mon, 19 Feb 2024 23:27:45 +0800 Subject: [PATCH 25/38] Fix a lint issue --- .../android/ui/notifications/NotificationsListFragmentPage.kt | 1 - 1 file changed, 1 deletion(-) 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 index bd4817a88b56..6b3d4afc7a53 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -74,7 +74,6 @@ import org.wordpress.android.util.helpers.SwipeToRefreshHelper import org.wordpress.android.widgets.AppRatingDialog.incrementInteractions import javax.inject.Inject - @AndroidEntryPoint class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_list_fragment_page), OnScrollToTopListener { From 42354bcc8811e99dea9782692ec56930c64c503c Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Mon, 19 Feb 2024 17:10:56 +0000 Subject: [PATCH 26/38] Bump version number --- version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.properties b/version.properties index b90452d495f1..6d000f7ebe56 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -versionName=24.2 -versionCode=1410 +versionName=24.3-rc-1 +versionCode=1411 \ No newline at end of file From b7332b67b4b83c621b2e6b900e046efa6f721e8b Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Mon, 19 Feb 2024 17:10:56 +0000 Subject: [PATCH 27/38] Update draft release notes for 24.3. --- WordPress/metadata/release_notes.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/metadata/release_notes.txt b/WordPress/metadata/release_notes.txt index ec82690d1a47..be8790a36766 100644 --- a/WordPress/metadata/release_notes.txt +++ b/WordPress/metadata/release_notes.txt @@ -1,3 +1,3 @@ -We fixed an issue that made images and other media blink away while being uploaded. Presto, no more disappearing act. +* [**] 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] -The editor won’t crash anymore when you’re working on large posts. That’s right, we’ve saved your drafts and your sanity. From bd68d961f2a9a1aeb3e5d1a7a9e21e9c3bb09613 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Mon, 19 Feb 2024 17:10:56 +0000 Subject: [PATCH 28/38] Update draft release notes for Jetpack 24.3. --- WordPress/jetpack_metadata/release_notes.txt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/WordPress/jetpack_metadata/release_notes.txt b/WordPress/jetpack_metadata/release_notes.txt index 1f7c6151d46d..be8790a36766 100644 --- a/WordPress/jetpack_metadata/release_notes.txt +++ b/WordPress/jetpack_metadata/release_notes.txt @@ -1,4 +1,3 @@ -- We added a new look and feel for content navigation and filtering. -- Images and other media won’t “blink” during upload. -- The editor won’t crash when you’re working on a large post. -- We added new Site Monitoring menu items like metrics, PHP logs, and web server logs. +* [**] 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] + From e3cea94b1e1374f2c562bb3cac07aa45c6b31386 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Mon, 19 Feb 2024 17:10:56 +0000 Subject: [PATCH 29/38] Release Notes: add new section for next version (24.4) --- RELEASE-NOTES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index be79913d54c1..77db9de8a38a 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,5 +1,9 @@ *** PLEASE FOLLOW THIS FORMAT: [] [] +24.4 +----- + + 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] From ee3efa95db5b3577b37ca11bb130625c84aaaef5 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Mon, 19 Feb 2024 12:56:45 -0500 Subject: [PATCH 30/38] Update FluxC version to 2.67.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1c8b4872bc6c..6f97761b1182 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { automatticTracksVersion = '3.4.0' gutenbergMobileVersion = 'v1.112.0' wordPressAztecVersion = 'v2.0' - wordPressFluxCVersion = 'trunk-ed60798b4d96ec19863c74b0f525e2e20f4525db' + wordPressFluxCVersion = '2.67.0' wordPressLoginVersion = 'trunk-a90b1ce939aba700d822f188d41624385f9c1dce' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.13.0' From fae27dd13814ef426ea28e423045e5c8446f0900 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Mon, 19 Feb 2024 12:57:09 -0500 Subject: [PATCH 31/38] Update login library to 1.14.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6f97761b1182..c38978d64ec4 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ ext { gutenbergMobileVersion = 'v1.112.0' wordPressAztecVersion = 'v2.0' wordPressFluxCVersion = '2.67.0' - wordPressLoginVersion = 'trunk-a90b1ce939aba700d822f188d41624385f9c1dce' + wordPressLoginVersion = '1.14.0' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.13.0' indexosMediaForMobileVersion = '43a9026f0973a2f0a74fa813132f6a16f7499c3a' From 255ce6a90db9f759796bfc59c79464d0b9ea59af Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Mon, 19 Feb 2024 12:58:09 -0500 Subject: [PATCH 32/38] Remove Jetpack-only release notes from WordPress 24.3 release notes --- WordPress/metadata/release_notes.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/WordPress/metadata/release_notes.txt b/WordPress/metadata/release_notes.txt index be8790a36766..d16d3ea03efd 100644 --- a/WordPress/metadata/release_notes.txt +++ b/WordPress/metadata/release_notes.txt @@ -1,3 +1,2 @@ * [**] 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] From bff4d5e232564dae5abbaa47447190a7c379369f Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Mon, 19 Feb 2024 14:59:34 -0500 Subject: [PATCH 33/38] Bundle update --- Gemfile.lock | 82 +++++++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5667a5c2567a..651960877cce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,11 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - activesupport (7.1.2) + activesupport (7.1.3) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -19,24 +21,25 @@ GEM ast (2.4.2) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.855.0) - aws-sdk-core (3.188.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (1.894.0) + aws-sdk-core (3.191.2) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) + base64 jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.73.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-kms (1.77.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.139.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-s3 (1.143.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.7.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) - bigdecimal (3.1.4) + bigdecimal (3.1.6) buildkit (1.5.0) sawyer (>= 0.6) chroma (0.2.0) @@ -49,7 +52,7 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) cork (0.3.0) colored2 (~> 3.1) @@ -86,19 +89,19 @@ GEM danger rake (> 10) thor (~> 1.0.0) - danger-xcode_summary (1.2.0) + danger-xcode_summary (1.3.0) danger-plugin-api (~> 1.0) xcresult (~> 0.2) declarative (0.0.20) diffy (3.4.2) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20231109) + domain_name (0.6.20240107) dotenv (2.8.1) drb (2.2.0) ruby2_keywords emoji_regex (3.2.3) - excon (0.104.0) + excon (0.109.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -129,8 +132,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.7) - fastlane (2.217.0) + fastimage (2.3.0) + fastlane (2.219.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -149,6 +152,7 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -157,7 +161,7 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) @@ -170,7 +174,7 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-wpmreleasetoolkit (9.2.0) + fastlane-plugin-wpmreleasetoolkit (9.4.0) activesupport (>= 6.1.7.1) buildkit (~> 1.5) chroma (= 0.2.0) @@ -179,7 +183,7 @@ GEM git (~> 1.3) google-cloud-storage (~> 1.31) java-properties (~> 0.3.0) - nokogiri (~> 1.11) + nokogiri (~> 1.11, < 1.16) octokit (~> 6.1) parallel (~> 1.14) plist (~> 3.1) @@ -191,9 +195,9 @@ GEM git (1.19.1) addressable (~> 2.8) rchardet (~> 1.8) - google-apis-androidpublisher_v3 (0.53.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.2) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -201,24 +205,23 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) + google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -237,7 +240,8 @@ GEM java-properties (0.3.0) jmespath (1.6.2) json (2.7.1) - jwt (2.7.1) + jwt (2.8.0) + base64 kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) @@ -246,13 +250,14 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.20.0) + minitest (5.22.2) multi_json (1.15.0) multipart-post (2.4.0) mutex_m (0.2.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) + nkf (0.2.0) no_proxy_fix (0.1.2) nokogiri (1.15.5) mini_portile2 (~> 2.8.2) @@ -262,14 +267,14 @@ GEM sawyer (~> 0.9) open4 (1.3.4) options (2.3.2) - optparse (0.1.1) + optparse (0.4.0) os (1.1.4) ox (2.14.17) parallel (1.24.0) parser (3.3.0.5) ast (~> 2.4.1) racc - plist (3.7.0) + plist (3.7.1) progress_bar (1.3.3) highline (>= 1.6, < 3) options (~> 2.3.0) @@ -277,7 +282,7 @@ GEM racc (1.7.3) rainbow (3.1.1) rake (13.1.0) - rake-compiler (1.2.5) + rake-compiler (1.2.7) rake rchardet (1.8.0) regexp_parser (2.9.0) @@ -309,7 +314,7 @@ GEM addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) security (0.1.3) - signet (0.18.0) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -323,16 +328,15 @@ GEM thor (1.0.1) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) unicode-display_width (2.5.0) - webrick (1.8.1) word_wrap (1.0.0) - xcodeproj (1.23.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) From 9371c0e6d6477ca5683ac1db4ac4271d83e367c1 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Mon, 19 Feb 2024 20:02:27 +0000 Subject: [PATCH 34/38] Freeze strings for translation --- fastlane/resources/values/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fastlane/resources/values/strings.xml b/fastlane/resources/values/strings.xml index 9b1825e36e98..dc4df0c3b0cc 100644 --- a/fastlane/resources/values/strings.xml +++ b/fastlane/resources/values/strings.xml @@ -2869,6 +2869,11 @@ You have active premium upgrades on your site. Please cancel your upgrades prior to deleting your site. Show purchases Checking purchases + Your WordPress.com profile is powered by Gravatar + Updating your avatar, name, and about info here will also update it across all sites that use Gravatar profiles. + What is Gravatar? + Updates might take some time to sync with your Gravatar profile. + Done Account Settings @@ -4880,4 +4885,7 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> PHP Logs Web Server Logs We cannot open site monitoring at the moment. Please try again later + + + Site not found. Check that you are logged into the correct account. From 6ecfa6d6a6d413db244652490f142ea6eee876e9 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Tue, 20 Feb 2024 09:59:16 +0800 Subject: [PATCH 35/38] Fix a nullable context Co-authored-by: Antonis Lilis --- .../android/ui/notifications/NotificationsListFragmentPage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 6b3d4afc7a53..2cd58ce00ecc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -131,7 +131,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l .launchIn(viewLifecycleOwner.lifecycleScope) } binding = NotificationsListFragmentPageBinding.bind(view).apply { - notificationsList.layoutManager = LinearLayoutManagerWrapper(activity) + notificationsList.layoutManager = LinearLayoutManagerWrapper(view.context) notificationsList.adapter = notesAdapter swipeToRefreshHelper = WPSwipeToRefreshHelper.buildSwipeToRefreshHelper(notificationsRefresh) { hideNewNotificationsBar() From 77b6277cfaddc051c17a8d725bf1638a8206ccb2 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Tue, 20 Feb 2024 16:09:11 +0800 Subject: [PATCH 36/38] Remove a workaround and use a simple solution --- .../NotificationsListFragmentPage.kt | 26 +------------------ .../ui/notifications/adapters/NotesAdapter.kt | 23 +++++----------- 2 files changed, 8 insertions(+), 41 deletions(-) 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 index 2cd58ce00ecc..bdfa63c9386f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -1,11 +1,9 @@ package org.wordpress.android.ui.notifications import android.app.Activity -import android.content.Context import android.content.Intent import android.os.Bundle import android.text.TextUtils -import android.util.AttributeSet import android.view.View import android.view.animation.Animation import android.view.animation.Animation.AnimationListener @@ -131,7 +129,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l .launchIn(viewLifecycleOwner.lifecycleScope) } binding = NotificationsListFragmentPageBinding.bind(view).apply { - notificationsList.layoutManager = LinearLayoutManagerWrapper(view.context) + notificationsList.layoutManager = LinearLayoutManager(view.context) notificationsList.adapter = notesAdapter swipeToRefreshHelper = WPSwipeToRefreshHelper.buildSwipeToRefreshHelper(notificationsRefresh) { hideNewNotificationsBar() @@ -556,26 +554,4 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL) } } - - /** - * LinearLayoutManagerWrapper is a workaround for a bug in RecyclerView that causes IndexOutOfBoundsException - * @see [link](https://stackoverflow.com/questions/31759171/recyclerview-and-java-lang-indexoutofboundsexception-inconsistency-detected-in) - */ - internal class LinearLayoutManagerWrapper : LinearLayoutManager { - constructor(context: Context) : super(context) - constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super( - context, - orientation, - reverseLayout - ) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super( - context, - attrs, - defStyleAttr, - defStyleRes - ) - - override fun supportsPredictiveItemAnimations(): Boolean = false - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 14c365d1ed56..b7c4f840b82e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -3,8 +3,6 @@ package org.wordpress.android.ui.notifications.adapters import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil.ItemCallback import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -48,21 +46,14 @@ class NotesAdapter(context: Context, private val inlineActionEvents: MutableShar * Add notes to the adapter and notify the change */ fun addAll(notes: List) = coroutineScope.launch { + val currentSize: Int = filteredNotes.size + val newNotes = buildFilteredNotesList(notes, currentFilter) + filteredNotes.clear() + filteredNotes.addAll(newNotes) withContext(Dispatchers.Main) { - val newNotes = buildFilteredNotesList(notes, currentFilter) - val differ = AsyncListDiffer(this@NotesAdapter, object : ItemCallback() { - override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean = - oldItem.id == newItem.id - - override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean = - oldItem.json.toString() == newItem.json.toString() - }) - - filteredNotes.clear() - filteredNotes.addAll(newNotes) - - differ.submitList(newNotes) - onNotesLoaded(itemCount) + notifyItemRangeRemoved(0, currentSize) + notifyItemRangeInserted(0, newNotes.size) + onNotesLoaded(newNotes.size) } } From 4e91555ef3ae0efe58ddf6145899bc624ecf029a Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Tue, 20 Feb 2024 16:34:53 +0800 Subject: [PATCH 37/38] Add the workaround back --- .../NotificationsListFragmentPage.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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 index bdfa63c9386f..60ab67d67b4c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -1,9 +1,11 @@ package org.wordpress.android.ui.notifications import android.app.Activity +import android.content.Context import android.content.Intent import android.os.Bundle import android.text.TextUtils +import android.util.AttributeSet import android.view.View import android.view.animation.Animation import android.view.animation.Animation.AnimationListener @@ -129,7 +131,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l .launchIn(viewLifecycleOwner.lifecycleScope) } binding = NotificationsListFragmentPageBinding.bind(view).apply { - notificationsList.layoutManager = LinearLayoutManager(view.context) + notificationsList.layoutManager = LinearLayoutManagerWrapper(view.context) notificationsList.adapter = notesAdapter swipeToRefreshHelper = WPSwipeToRefreshHelper.buildSwipeToRefreshHelper(notificationsRefresh) { hideNewNotificationsBar() @@ -554,4 +556,22 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL) } } + + internal class LinearLayoutManagerWrapper : LinearLayoutManager { + constructor(context: Context) : super(context) + constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super( + context, + orientation, + reverseLayout + ) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super( + context, + attrs, + defStyleAttr, + defStyleRes + ) + + override fun supportsPredictiveItemAnimations(): Boolean = false + } } From 7e8f3ef11ecf741430f8f24b12d0dcc378f73f69 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Tue, 20 Feb 2024 16:40:30 +0800 Subject: [PATCH 38/38] Add a comment --- .../android/ui/notifications/NotificationsListFragmentPage.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 index 60ab67d67b4c..fded7ceb28ba 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -557,6 +557,10 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l } } + /** + * LinearLayoutManagerWrapper is a workaround for a bug in RecyclerView that blocks the UI thread + * when we perform the first click on the inline actions in the notifications list. + */ internal class LinearLayoutManagerWrapper : LinearLayoutManager { constructor(context: Context) : super(context) constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(