From 92cb7b6f3b3580dda69b580a427168c9f2b8cddb Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Mon, 18 Sep 2023 13:22:04 +0200 Subject: [PATCH 01/88] rename notifications package to systemnotifications --- app/src/main/java/com/keylesspalace/tusky/MainActivity.kt | 8 ++++---- .../main/java/com/keylesspalace/tusky/TuskyApplication.kt | 2 +- .../components/preference/AccountPreferencesFragment.kt | 2 +- .../preference/NotificationPreferencesFragment.kt | 2 +- .../NotificationFetcher.kt | 4 ++-- .../NotificationHelper.java | 2 +- .../PushNotificationHelper.kt | 2 +- .../tusky/fragment/NotificationsFragment.java | 2 +- .../receiver/NotificationBlockStateBroadcastReceiver.kt | 6 +++--- .../tusky/receiver/SendStatusBroadcastReceiver.kt | 2 +- .../tusky/receiver/UnifiedPushBroadcastReceiver.kt | 4 ++-- .../com/keylesspalace/tusky/service/SendStatusService.kt | 2 +- .../java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt | 4 ++-- .../com/keylesspalace/tusky/worker/NotificationWorker.kt | 6 +++--- .../com/keylesspalace/tusky/worker/PruneCacheWorker.kt | 4 ++-- .../test/java/com/keylesspalace/tusky/MainActivityTest.kt | 2 +- 16 files changed, 27 insertions(+), 27 deletions(-) rename app/src/main/java/com/keylesspalace/tusky/components/{notifications => systemnotifications}/NotificationFetcher.kt (98%) rename app/src/main/java/com/keylesspalace/tusky/components/{notifications => systemnotifications}/NotificationHelper.java (99%) rename app/src/main/java/com/keylesspalace/tusky/components/{notifications => systemnotifications}/PushNotificationHelper.kt (99%) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 090c9e2f65..573d4b0c14 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -77,10 +77,10 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType import com.keylesspalace.tusky.components.drafts.DraftsActivity import com.keylesspalace.tusky.components.login.LoginActivity -import com.keylesspalace.tusky.components.notifications.NotificationHelper -import com.keylesspalace.tusky.components.notifications.disableAllNotifications -import com.keylesspalace.tusky.components.notifications.enablePushNotificationsWithFallback -import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.disableAllNotifications +import com.keylesspalace.tusky.components.systemnotifications.enablePushNotificationsWithFallback +import com.keylesspalace.tusky.components.systemnotifications.showMigrationNoticeIfNecessary import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity import com.keylesspalace.tusky.components.search.SearchActivity diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index 84fbabbaf2..a1dfcf1130 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -23,7 +23,7 @@ import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import autodispose2.AutoDisposePlugins -import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.settings.NEW_INSTALL_SCHEMA_VERSION import com.keylesspalace.tusky.settings.PrefKeys diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt index 629c3709d5..c5bf3fe984 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt @@ -34,7 +34,7 @@ import com.keylesspalace.tusky.components.domainblocks.DomainBlocksActivity import com.keylesspalace.tusky.components.filters.FiltersActivity import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity import com.keylesspalace.tusky.components.login.LoginActivity -import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration +import com.keylesspalace.tusky.components.systemnotifications.currentAccountNeedsMigration import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Account diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt index 83a96ed5b8..d95b68b680 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt @@ -18,7 +18,7 @@ package com.keylesspalace.tusky.components.preference import android.os.Bundle import androidx.preference.PreferenceFragmentCompat import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt similarity index 98% rename from app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt rename to app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt index 5083ea9b14..5da34c89eb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt @@ -1,10 +1,10 @@ -package com.keylesspalace.tusky.components.notifications +package com.keylesspalace.tusky.components.systemnotifications import android.app.NotificationManager import android.content.Context import android.util.Log import androidx.annotation.WorkerThread -import com.keylesspalace.tusky.components.notifications.NotificationHelper.filterNotification +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper.filterNotification import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Marker diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.java similarity index 99% rename from app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java rename to app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.java index 15a39780fb..0f98258fa3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.components.notifications; +package com.keylesspalace.tusky.components.systemnotifications; import static com.keylesspalace.tusky.BuildConfig.APPLICATION_ID; import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml; diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/PushNotificationHelper.kt similarity index 99% rename from app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt rename to app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/PushNotificationHelper.kt index f61307e3eb..2048651b5f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/PushNotificationHelper.kt @@ -15,7 +15,7 @@ @file:JvmName("PushNotificationHelper") -package com.keylesspalace.tusky.components.notifications +package com.keylesspalace.tusky.components.systemnotifications import android.app.NotificationManager import android.content.Context diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index f6df6a8552..413d282b50 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -67,7 +67,7 @@ import com.keylesspalace.tusky.appstore.PinEvent; import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; import com.keylesspalace.tusky.appstore.ReblogEvent; -import com.keylesspalace.tusky.components.notifications.NotificationHelper; +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper; import com.keylesspalace.tusky.databinding.FragmentTimelineNotificationsBinding; import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt index 20b18a9f97..f8742f11ec 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt @@ -20,9 +20,9 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build -import com.keylesspalace.tusky.components.notifications.canEnablePushNotifications -import com.keylesspalace.tusky.components.notifications.isUnifiedPushNotificationEnabledForAccount -import com.keylesspalace.tusky.components.notifications.updateUnifiedPushSubscription +import com.keylesspalace.tusky.components.systemnotifications.canEnablePushNotifications +import com.keylesspalace.tusky.components.systemnotifications.isUnifiedPushNotificationEnabledForAccount +import com.keylesspalace.tusky.components.systemnotifications.updateUnifiedPushSubscription import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.network.MastodonApi import dagger.android.AndroidInjection diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index 861edae604..48e0859018 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -24,7 +24,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.service.SendStatusService diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt index b95e53105a..89a3da11dd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt @@ -20,8 +20,8 @@ import android.content.Intent import android.util.Log import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager -import com.keylesspalace.tusky.components.notifications.registerUnifiedPushEndpoint -import com.keylesspalace.tusky.components.notifications.unregisterUnifiedPushEndpoint +import com.keylesspalace.tusky.components.systemnotifications.registerUnifiedPushEndpoint +import com.keylesspalace.tusky.components.systemnotifications.unregisterUnifiedPushEndpoint import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.worker.NotificationWorker diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt index 3aaad1b704..aa0d58b466 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -27,7 +27,7 @@ import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.components.compose.MediaUploader import com.keylesspalace.tusky.components.compose.UploadEvent import com.keylesspalace.tusky.components.drafts.DraftHelper -import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Attachment diff --git a/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt index f8d3b11cae..2d3460d834 100644 --- a/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt @@ -2,8 +2,8 @@ package com.keylesspalace.tusky.usecase import android.content.Context import com.keylesspalace.tusky.components.drafts.DraftHelper -import com.keylesspalace.tusky.components.notifications.NotificationHelper -import com.keylesspalace.tusky.components.notifications.disableUnifiedPushNotificationsForAccount +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.disableUnifiedPushNotificationsForAccount import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt index cc99b78b1d..f154f26dd5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt @@ -23,9 +23,9 @@ import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.components.notifications.NotificationFetcher -import com.keylesspalace.tusky.components.notifications.NotificationHelper -import com.keylesspalace.tusky.components.notifications.NotificationHelper.NOTIFICATION_ID_FETCH_NOTIFICATION +import com.keylesspalace.tusky.components.systemnotifications.NotificationFetcher +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper.NOTIFICATION_ID_FETCH_NOTIFICATION import javax.inject.Inject /** Fetch and show new notifications. */ diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt index 5a65a2ef91..ee899e7a92 100644 --- a/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt @@ -25,8 +25,8 @@ import androidx.work.ForegroundInfo import androidx.work.ListenableWorker import androidx.work.WorkerParameters import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.components.notifications.NotificationHelper -import com.keylesspalace.tusky.components.notifications.NotificationHelper.NOTIFICATION_ID_PRUNE_CACHE +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper.NOTIFICATION_ID_PRUNE_CACHE import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import javax.inject.Inject diff --git a/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt index 8ad20a411c..227964246d 100644 --- a/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt @@ -11,7 +11,7 @@ import androidx.work.testing.WorkManagerTestInitHelper import at.connyduck.calladapter.networkresult.NetworkResult import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.components.accountlist.AccountListActivity -import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Notification From 2a557576df629af5a6c21967c515f1fddfe6e032 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Mon, 18 Sep 2023 14:35:16 +0200 Subject: [PATCH 02/88] add the "showNotificationsFilter" preference back --- .../main/java/com/keylesspalace/tusky/TuskyApplication.kt | 6 ------ .../tusky/components/preference/PreferencesFragment.kt | 7 +++++++ .../com/keylesspalace/tusky/settings/SettingsConstants.kt | 3 ++- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index a1dfcf1130..efdaa90358 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -133,12 +133,6 @@ class TuskyApplication : Application(), HasAndroidInjector { editor.remove(PrefKeys.MEDIA_PREVIEW_ENABLED) } - if (oldVersion < 2023072401) { - // The notifications filter / clear options are shown on a menu, not a separate bar, - // the preference to display them is not needed. - editor.remove(PrefKeys.Deprecated.SHOW_NOTIFICATIONS_FILTER) - } - if (oldVersion != NEW_INSTALL_SCHEMA_VERSION && oldVersion < 2023082301) { // Default value for appTheme is now THEME_SYSTEM. If the user is upgrading and // didn't have an explicit preference set use the previous default, so the diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt index f6541f1fdc..cb9843f24e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -158,6 +158,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { setTitle(R.string.pref_title_hide_top_toolbar) } + switchPreference { + setDefaultValue(true) + key = PrefKeys.SHOW_NOTIFICATIONS_FILTER + setTitle(R.string.pref_title_show_notifications_filter) + isSingleLineTitle = false + } + switchPreference { setDefaultValue(false) key = PrefKeys.FAB_HIDE diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt index 4782601a56..adbdb29de6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -59,6 +59,7 @@ object PrefKeys { const val READING_ORDER = "readingOrder" const val MAIN_NAV_POSITION = "mainNavPosition" const val HIDE_TOP_TOOLBAR = "hideTopToolbar" + const val SHOW_NOTIFICATIONS_FILTER = "showNotificationsFilter" const val ABSOLUTE_TIME_VIEW = "absoluteTimeView" const val SHOW_BOT_OVERLAY = "showBotOverlay" const val ANIMATE_GIF_AVATARS = "animateGifAvatars" @@ -109,6 +110,6 @@ object PrefKeys { /** Keys that are no longer used (e.g., the preference has been removed */ object Deprecated { - const val SHOW_NOTIFICATIONS_FILTER = "showNotificationsFilter" + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3f1165f4ee..90c41c9706 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -683,7 +683,7 @@ Enable swipe gesture to switch between tabs Show post statistics in timeline - + Show Notifications filter Poll Duration From cfc8a40e21ad1009e9e6491990e7f0a7a34f3808 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Wed, 20 Sep 2023 19:50:18 +0200 Subject: [PATCH 03/88] Rename .java to .kt --- .../{NotificationViewData.java => NotificationViewData.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/keylesspalace/tusky/viewdata/{NotificationViewData.java => NotificationViewData.kt} (100%) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt similarity index 100% rename from app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java rename to app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt From 075f76b86b20204936a95d93e49e80777c12a6df Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Wed, 20 Sep 2023 19:50:18 +0200 Subject: [PATCH 04/88] add new NotificationsFragment, implement NotificationsPagingAdapter --- .../tusky/adapter/FollowRequestViewHolder.kt | 13 +- .../tusky/adapter/NotificationsAdapter.java | 46 +-- .../tusky/adapter/PlaceholderViewHolder.kt | 60 +-- .../tusky/adapter/StatusViewHolder.java | 2 +- .../notifications/FollowViewHolder.kt | 51 +++ .../notifications/NotificationsFragment.kt | 154 ++++++++ .../NotificationsPagingAdapter.kt | 174 ++++++++ .../ReportNotificationViewHolder.kt | 36 +- .../StatusNotificationViewHolder.kt | 374 ++++++++++++++++++ .../notifications/StatusViewHolder.kt | 59 +++ .../UnknownNotificationViewHolder.kt | 39 ++ .../timeline/TimelinePagingAdapter.kt | 5 +- .../tusky/fragment/NotificationsFragment.java | 54 +-- .../keylesspalace/tusky/util/ViewDataUtils.kt | 6 +- .../tusky/viewdata/NotificationViewData.kt | 137 +------ .../tusky/viewdata/NotificationViewData2.java | 138 +++++++ .../res/layout/item_status_placeholder.xml | 59 +-- .../res/layout/item_unknown_notification.xml | 11 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 6 - 20 files changed, 1148 insertions(+), 278 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/FollowViewHolder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt rename app/src/main/java/com/keylesspalace/tusky/{adapter => components/notifications}/ReportNotificationViewHolder.kt (78%) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/UnknownNotificationViewHolder.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData2.java create mode 100644 app/src/main/res/layout/item_unknown_notification.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt index 2bbdf44e80..53f391ce1e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -21,10 +21,12 @@ import android.text.Spanned import android.text.style.StyleSpan import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.notifications.NotificationsViewHolder import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.loadAvatar @@ -33,12 +35,21 @@ import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.unicodeWrap import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.viewdata.NotificationViewData class FollowRequestViewHolder( private val binding: ItemFollowRequestBinding, private val linkListener: LinkListener, private val showHeader: Boolean -) : RecyclerView.ViewHolder(binding.root) { +) : RecyclerView.ViewHolder(binding.root), NotificationsViewHolder { + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>?, + statusDisplayOptions: StatusDisplayOptions + ) { + TODO("Not yet implemented") + } fun setupWithAccount( account: TimelineAccount, diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 1794645435..e065454c26 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -41,8 +41,10 @@ import com.bumptech.glide.Glide; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.components.notifications.ReportNotificationViewHolder; import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding; import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding; +import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding; import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Status; @@ -59,7 +61,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.util.TimestampUtils; -import com.keylesspalace.tusky.viewdata.NotificationViewData; +import com.keylesspalace.tusky.viewdata.NotificationViewData2; import com.keylesspalace.tusky.viewdata.StatusViewData; import java.util.Date; @@ -92,11 +94,11 @@ public interface AdapterDataSource { private final StatusActionListener statusListener; private final NotificationActionListener notificationActionListener; private final AccountActionListener accountActionListener; - private final AdapterDataSource dataSource; + private final AdapterDataSource dataSource; private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter(); public NotificationsAdapter(String accountId, - AdapterDataSource dataSource, + AdapterDataSource dataSource, StatusDisplayOptions statusDisplayOptions, StatusActionListener statusListener, NotificationActionListener notificationActionListener, @@ -137,11 +139,11 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int case VIEW_TYPE_PLACEHOLDER: { View view = inflater .inflate(R.layout.item_status_placeholder, parent, false); - return new PlaceholderViewHolder(view); + return new PlaceholderViewHolder(ItemStatusPlaceholderBinding.inflate(inflater, parent, false), statusListener); } case VIEW_TYPE_REPORT: { ItemReportNotificationBinding binding = ItemReportNotificationBinding.inflate(inflater, parent, false); - return new ReportNotificationViewHolder(binding); + return new ReportNotificationViewHolder(binding, notificationActionListener); } default: case VIEW_TYPE_UNKNOWN: { @@ -171,17 +173,17 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int po private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) { Object payloadForHolder = payloads != null && !payloads.isEmpty() ? payloads.get(0) : null; if (position < this.dataSource.getItemCount()) { - NotificationViewData notification = dataSource.getItemAt(position); - if (notification instanceof NotificationViewData.Placeholder) { + NotificationViewData2 notification = dataSource.getItemAt(position); + if (notification instanceof NotificationViewData2.Placeholder) { if (payloadForHolder == null) { - NotificationViewData.Placeholder placeholder = ((NotificationViewData.Placeholder) notification); + NotificationViewData2.Placeholder placeholder = ((NotificationViewData2.Placeholder) notification); PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder; - holder.setup(statusListener, placeholder.isLoading()); + holder.setup(placeholder.isLoading()); } return; } - NotificationViewData.Concrete concreteNotification = - (NotificationViewData.Concrete) notification; + NotificationViewData2.Concrete concreteNotification = + (NotificationViewData2.Concrete) notification; switch (viewHolder.getItemViewType()) { case VIEW_TYPE_STATUS: { StatusViewHolder holder = (StatusViewHolder) viewHolder; @@ -261,8 +263,8 @@ private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int pos case VIEW_TYPE_REPORT: { if (payloadForHolder == null) { ReportNotificationViewHolder holder = (ReportNotificationViewHolder) viewHolder; - holder.setupWithReport(concreteNotification.getAccount(), concreteNotification.getReport(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis()); - holder.setupActionListener(notificationActionListener, concreteNotification.getReport().getTargetAccount().getId(), concreteNotification.getAccount().getId(), concreteNotification.getReport().getId()); + // holder.setupWithReport(concreteNotification.getAccount(), concreteNotification.getReport(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis()); + // holder.setupActionListener(notificationActionListener, concreteNotification.getReport().getTargetAccount().getId(), concreteNotification.getAccount().getId(), concreteNotification.getReport().getId()); } } default: @@ -299,9 +301,9 @@ public boolean isMediaPreviewEnabled() { @Override public int getItemViewType(int position) { - NotificationViewData notification = dataSource.getItemAt(position); - if (notification instanceof NotificationViewData.Concrete) { - NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification); + NotificationViewData2 notification = dataSource.getItemAt(position); + if (notification instanceof NotificationViewData2.Concrete) { + NotificationViewData2.Concrete concrete = ((NotificationViewData2.Concrete) notification); switch (concrete.getType()) { case MENTION: case POLL: { @@ -327,7 +329,7 @@ public int getItemViewType(int position) { return VIEW_TYPE_UNKNOWN; } } - } else if (notification instanceof NotificationViewData.Placeholder) { + } else if (notification instanceof NotificationViewData2.Placeholder) { return VIEW_TYPE_PLACEHOLDER; } else { throw new AssertionError("Unknown notification type"); @@ -355,14 +357,14 @@ public interface NotificationActionListener { void onNotificationContentCollapsedChange(boolean isCollapsed, int position); } - private static class FollowViewHolder extends RecyclerView.ViewHolder { + public static class FollowViewHolder extends RecyclerView.ViewHolder { private final TextView message; private final TextView usernameView; private final TextView displayNameView; private final ImageView avatar; private final StatusDisplayOptions statusDisplayOptions; - FollowViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) { + public FollowViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) { super(itemView); message = itemView.findViewById(R.id.notification_text); usernameView = itemView.findViewById(R.id.notification_username); @@ -404,7 +406,7 @@ void setupButtons(final NotificationActionListener listener, final String accoun } } - private static class StatusNotificationViewHolder extends RecyclerView.ViewHolder + public static class StatusNotificationViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private final View container; @@ -431,7 +433,7 @@ private static class StatusNotificationViewHolder extends RecyclerView.ViewHolde private final int avatarRadius36dp; private final int avatarRadius24dp; - StatusNotificationViewHolder( + public StatusNotificationViewHolder( View itemView, StatusDisplayOptions statusDisplayOptions, AbsoluteTimeFormatter absoluteTimeFormatter @@ -522,7 +524,7 @@ Drawable getIconWithColor(Context context, @DrawableRes int drawable, @ColorRes return icon; } - void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) { + void setMessage(NotificationViewData2.Concrete notificationViewData, LinkListener listener) { this.statusViewData = notificationViewData.getStatusViewData(); String displayName = StringUtils.unicodeWrap(notificationViewData.getAccount().getName()); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt index c277ea385e..3ef4dcf6a2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt @@ -14,54 +14,34 @@ * see . */ package com.keylesspalace.tusky.adapter -import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.button.MaterialButton -import com.google.android.material.progressindicator.CircularProgressIndicatorSpec -import com.google.android.material.progressindicator.IndeterminateDrawable -import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.visible /** - * Placeholder for different timelines. + * Placeholder for missing parts in timelines. * - * Displays a "Load more" button for a particular status ID, or a - * circular progress wheel if the status' page is being loaded. - * - * The user can only have one "Load more" operation in progress at - * a time (determined by the adapter), so the contents of the view - * and the enabled state is driven by that. + * Displays a "Load more" button to load the gap, or a + * circular progress bar if the missing page is being loaded. */ -class PlaceholderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val loadMoreButton: MaterialButton = itemView.findViewById(R.id.button_load_more) - private val drawable = IndeterminateDrawable.createCircularDrawable( - itemView.context, - CircularProgressIndicatorSpec(itemView.context, null) - ) - - fun setup(listener: StatusActionListener, loading: Boolean) { - itemView.isEnabled = !loading - loadMoreButton.isEnabled = !loading - - if (loading) { - loadMoreButton.text = "" - loadMoreButton.icon = drawable - return - } +class PlaceholderViewHolder( + private val binding: ItemStatusPlaceholderBinding, + private val listener: StatusActionListener +) : RecyclerView.ViewHolder(binding.root) { - loadMoreButton.text = itemView.context.getString(R.string.load_more_placeholder_text) - loadMoreButton.icon = null + fun setup(loading: Boolean) { + binding.loadMoreButton.visible(!loading) + binding.loadMoreProgressBar.visible(loading) - // To allow the user to click anywhere in the layout to load more content set the click - // listener on the parent layout instead of loadMoreButton. - // - // See the comments in item_status_placeholder.xml for more details. - itemView.setOnClickListener { - itemView.isEnabled = false - loadMoreButton.isEnabled = false - loadMoreButton.icon = drawable - loadMoreButton.text = "" - listener.onLoadMore(bindingAdapterPosition) + if (!loading) { + binding.loadMoreButton.setOnClickListener { + binding.loadMoreButton.hide() + binding.loadMoreProgressBar.show() + listener.onLoadMore(bindingAdapterPosition) + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 327f7cfbb8..5d300f5d38 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -105,7 +105,7 @@ private void setRebloggedByDisplayName(final CharSequence name, } // don't use this on the same ViewHolder as setRebloggedByDisplayName, will cause recycling issues as paddings are changed - protected void setPollInfo(final boolean ownPoll) { + public void setPollInfo(final boolean ownPoll) { statusInfo.setText(ownPoll ? R.string.poll_ended_created : R.string.poll_ended_voted); statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_poll_24dp, 0, 0, 0); statusInfo.setCompoundDrawablePadding(Utils.dpToPx(statusInfo.getContext(), 10)); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/FollowViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/FollowViewHolder.kt new file mode 100644 index 0000000000..c03a692196 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/FollowViewHolder.kt @@ -0,0 +1,51 @@ +package com.keylesspalace.tusky.components.notifications + +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.NotificationsAdapter.NotificationActionListener +import com.keylesspalace.tusky.databinding.ItemFollowBinding +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.viewdata.NotificationViewData + +class FollowViewHolder( + private val binding: ItemFollowBinding, + private val listener: NotificationActionListener, +) : RecyclerView.ViewHolder(binding.root), NotificationsViewHolder { + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>?, + statusDisplayOptions: StatusDisplayOptions + ) { + val context = itemView.context + val account = viewData.account + val messageTemplate = + context.getString(if (viewData.type == Notification.Type.SIGN_UP) R.string.notification_sign_up_format else R.string.notification_follow_format) + val wrappedDisplayName = account.name.unicodeWrap() + + binding.notificationText.text = messageTemplate.format(wrappedDisplayName) + .emojify(account.emojis, binding.notificationText, statusDisplayOptions.animateEmojis) + + binding.notificationUsername.text = context.getString(R.string.post_username_format, viewData.account.username) + + val emojifiedDisplayName = wrappedDisplayName.emojify( + account.emojis, + binding.notificationDisplayName, + statusDisplayOptions.animateEmojis + ) + binding.notificationDisplayName.text = emojifiedDisplayName + + val avatarRadius = context.resources + .getDimensionPixelSize(R.dimen.avatar_radius_42dp) + loadAvatar( + account.avatar, binding.notificationAvatar, avatarRadius, + statusDisplayOptions.animateAvatars, null + ) + + itemView.setOnClickListener { listener.onViewAccount(account.id) } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt new file mode 100644 index 0000000000..ff3e567b96 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt @@ -0,0 +1,154 @@ +package com.keylesspalace.tusky.components.notifications + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView.LayoutManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.FragmentTimelineNotificationsBinding +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate +import com.keylesspalace.tusky.util.StatusProvider +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.viewdata.NotificationViewData + +class NotificationsFragment: Fragment(), SwipeRefreshLayout.OnRefreshListener, + StatusActionListener { + + private val binding by viewBinding(FragmentTimelineNotificationsBinding::bind) + + private lateinit var layoutManager: LayoutManager + //private lateinit var adapter: NotificationsAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_timeline_notifications, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + + val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + + val showNotificationsFilter = preferences.getBoolean(PrefKeys.SHOW_NOTIFICATIONS_FILTER, true) + + // Setup the SwipeRefreshLayout. + + // Setup the SwipeRefreshLayout. + binding.swipeRefreshLayout.setOnRefreshListener(this) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + + // Setup the RecyclerView. + + // Setup the RecyclerView. + binding.recyclerView.setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.setAccessibilityDelegateCompat( + ListStatusAccessibilityDelegate(binding.recyclerView, this, StatusProvider { pos: Int -> + /* if (pos in 0 until adapter.itemCount) { + val notification = adapter.peek(pos) + // We support replies only for now + if (notification is NotificationViewData.Concrete) { + return@StatusProvider notification.statusViewData + } else { + return@StatusProvider null + } + } else { + null + }*/ + null + }) + ) + + binding.recyclerView.addItemDecoration( + DividerItemDecoration( + context, + DividerItemDecoration.VERTICAL + ) + ) + } + + override fun onRefresh() { + + + } + + override fun onViewAccount(id: String) { + TODO("Not yet implemented") + } + + override fun onViewUrl(url: String) { + TODO("Not yet implemented") + } + + override fun onViewTag(tag: String) { + TODO("Not yet implemented") + } + + override fun onReply(position: Int) { + TODO("Not yet implemented") + } + + override fun onReblog(reblog: Boolean, position: Int) { + TODO("Not yet implemented") + } + + override fun onFavourite(favourite: Boolean, position: Int) { + TODO("Not yet implemented") + } + + override fun onBookmark(bookmark: Boolean, position: Int) { + TODO("Not yet implemented") + } + + override fun onMore(view: View, position: Int) { + TODO("Not yet implemented") + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + TODO("Not yet implemented") + } + + override fun onViewThread(position: Int) { + TODO("Not yet implemented") + } + + override fun onOpenReblog(position: Int) { + TODO("Not yet implemented") + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + TODO("Not yet implemented") + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + TODO("Not yet implemented") + } + + override fun onLoadMore(position: Int) { + TODO("Not yet implemented") + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + TODO("Not yet implemented") + } + + override fun onVoteInPoll(position: Int, choices: MutableList) { + TODO("Not yet implemented") + } + + override fun clearWarningAction(position: Int) { + TODO("Not yet implemented") + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt new file mode 100644 index 0000000000..ce0e2ebdc7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt @@ -0,0 +1,174 @@ +package com.keylesspalace.tusky.components.notifications + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.adapter.FollowRequestViewHolder +import com.keylesspalace.tusky.adapter.NotificationsAdapter +import com.keylesspalace.tusky.adapter.PlaceholderViewHolder +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.databinding.ItemFollowBinding +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding +import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding +import com.keylesspalace.tusky.databinding.ItemStatusBinding +import com.keylesspalace.tusky.databinding.ItemStatusNotificationBinding +import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding +import com.keylesspalace.tusky.databinding.ItemUnknownNotificationBinding +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.AbsoluteTimeFormatter +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.NotificationViewData + +interface NotificationsViewHolder { + fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>?, + statusDisplayOptions: StatusDisplayOptions + ) +} + +class NotificationsPagingAdapter( + private val accountId: String, + private val statusDisplayOptions: StatusDisplayOptions, + private val linkListener: LinkListener, + private val statusListener: StatusActionListener, + private val notificationActionListener: NotificationsAdapter.NotificationActionListener, + private val accountActionListener: AccountActionListener +) + : PagingDataAdapter(NotificationsDifferCallback) { + + private val absoluteTimeFormatter = AbsoluteTimeFormatter() + + init { + stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY + } + + override fun getItemViewType(position: Int): Int { + return when (val notification = getItem(position)) { + is NotificationViewData.Concrete -> { + when (notification.type) { + Notification.Type.MENTION, + Notification.Type.POLL -> VIEW_TYPE_STATUS + Notification.Type.STATUS, + Notification.Type.FAVOURITE, + Notification.Type.REBLOG, + Notification.Type.UPDATE -> VIEW_TYPE_STATUS_NOTIFICATION + Notification.Type.FOLLOW, + Notification.Type.SIGN_UP -> VIEW_TYPE_FOLLOW + Notification.Type.FOLLOW_REQUEST -> VIEW_TYPE_FOLLOW_REQUEST + Notification.Type.REPORT -> VIEW_TYPE_REPORT + else -> VIEW_TYPE_UNKNOWN + } + } + is NotificationViewData.Placeholder -> VIEW_TYPE_PLACEHOLDER + null -> throw IllegalStateException("") + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + VIEW_TYPE_STATUS -> StatusViewHolder( + ItemStatusBinding.inflate(inflater, parent, false), + statusListener, + accountId + ) + VIEW_TYPE_STATUS_NOTIFICATION -> StatusNotificationViewHolder( + ItemStatusNotificationBinding.inflate(inflater, parent, false), + statusListener, + notificationActionListener, + absoluteTimeFormatter + ) + VIEW_TYPE_FOLLOW -> FollowViewHolder( + ItemFollowBinding.inflate(inflater, parent, false), + notificationActionListener + ) + VIEW_TYPE_FOLLOW_REQUEST -> FollowRequestViewHolder( + ItemFollowRequestBinding.inflate(inflater, parent, false), + linkListener, + true + ) + VIEW_TYPE_PLACEHOLDER -> PlaceholderViewHolder( + ItemStatusPlaceholderBinding.inflate(inflater, parent, false), + statusListener + ) + VIEW_TYPE_REPORT -> ReportNotificationViewHolder( + ItemReportNotificationBinding.inflate(inflater, parent, false), + notificationActionListener + ) + else -> UnknownNotificationViewHolder( + ItemUnknownNotificationBinding.inflate(inflater, parent, false) + ) + } + } + + override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { + bindViewHolder(viewHolder, position, null) + } + + override fun onBindViewHolder( + viewHolder: RecyclerView.ViewHolder, + position: Int, + payloads: List + ) { + bindViewHolder(viewHolder, position, payloads) + } + + private fun bindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int, payloads: List?) { + getItem(position)?.let { notification -> + when (notification) { + is NotificationViewData.Concrete -> + (viewHolder as NotificationsViewHolder).bind(notification, payloads, statusDisplayOptions) + is NotificationViewData.Placeholder -> { + (viewHolder as PlaceholderViewHolder).setup(notification.isLoading) + } + } + } + } + + companion object { + private const val VIEW_TYPE_STATUS = 0 + private const val VIEW_TYPE_STATUS_NOTIFICATION = 1 + private const val VIEW_TYPE_FOLLOW = 2 + private const val VIEW_TYPE_FOLLOW_REQUEST = 3 + private const val VIEW_TYPE_PLACEHOLDER = 4 + private const val VIEW_TYPE_REPORT = 5 + private const val VIEW_TYPE_UNKNOWN = 6 + + val NotificationsDifferCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: NotificationViewData, + newItem: NotificationViewData + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: NotificationViewData, + newItem: NotificationViewData + ): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload( + oldItem: NotificationViewData, + newItem: NotificationViewData + ): Any? { + return if (oldItem == newItem) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else { + // If items are different - update the whole view holder + null + } + } + } + } + +} + diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/ReportNotificationViewHolder.kt similarity index 78% rename from app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt rename to app/src/main/java/com/keylesspalace/tusky/components/notifications/ReportNotificationViewHolder.kt index db2f79a992..cf83a5cf3a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/ReportNotificationViewHolder.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.adapter +package com.keylesspalace.tusky.components.notifications import android.content.Context import androidx.core.content.ContextCompat @@ -22,21 +22,30 @@ import at.connyduck.sparkbutton.helpers.Utils import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.NotificationsAdapter.NotificationActionListener import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding -import com.keylesspalace.tusky.entity.Report -import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.getRelativeTimeSpanString import com.keylesspalace.tusky.util.loadAvatar import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.viewdata.NotificationViewData import java.util.Date class ReportNotificationViewHolder( private val binding: ItemReportNotificationBinding, -) : RecyclerView.ViewHolder(binding.root) { + private val listener: NotificationActionListener +) : RecyclerView.ViewHolder(binding.root), NotificationsViewHolder { - fun setupWithReport(reporter: TimelineAccount, report: Report, animateAvatar: Boolean, animateEmojis: Boolean) { - val reporterName = reporter.name.unicodeWrap().emojify(reporter.emojis, itemView, animateEmojis) - val reporteeName = report.targetAccount.name.unicodeWrap().emojify(report.targetAccount.emojis, itemView, animateEmojis) + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>?, + statusDisplayOptions: StatusDisplayOptions + ) { + + val report = viewData.report!! + val reporter = viewData.account + + val reporterName = reporter.name.unicodeWrap().emojify(reporter.emojis, itemView, statusDisplayOptions.animateEmojis) + val reporteeName = report.targetAccount.name.unicodeWrap().emojify(report.targetAccount.emojis, itemView, statusDisplayOptions.animateEmojis) val icon = ContextCompat.getDrawable(itemView.context, R.drawable.ic_flag_24dp) binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) @@ -52,37 +61,36 @@ class ReportNotificationViewHolder( report.targetAccount.avatar, binding.notificationReporteeAvatar, itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp), - animateAvatar, + statusDisplayOptions.animateAvatars, ) loadAvatar( reporter.avatar, binding.notificationReporterAvatar, itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp), - animateAvatar, + statusDisplayOptions.animateAvatars, ) - } - fun setupActionListener(listener: NotificationActionListener, reporteeId: String, reporterId: String, reportId: String) { binding.notificationReporteeAvatar.setOnClickListener { val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { - listener.onViewAccount(reporteeId) + listener.onViewAccount(report.targetAccount.id) } } binding.notificationReporterAvatar.setOnClickListener { val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { - listener.onViewAccount(reporterId) + listener.onViewAccount(reporter.id) } } - itemView.setOnClickListener { listener.onViewReport(reportId) } + itemView.setOnClickListener { listener.onViewReport(report.id) } } private fun getTranslatedCategory(context: Context, rawCategory: String): String { return when (rawCategory) { "violation" -> context.getString(R.string.report_category_violation) "spam" -> context.getString(R.string.report_category_spam) + "legal" -> context.getString(R.string.report_category_legal) "other" -> context.getString(R.string.report_category_other) else -> rawCategory } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt new file mode 100644 index 0000000000..a3809b3ca3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt @@ -0,0 +1,374 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . + */ + +package com.keylesspalace.tusky.components.notifications + +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.text.InputFilter +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextUtils +import android.text.format.DateUtils +import android.text.style.StyleSpan +import android.view.View +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import at.connyduck.sparkbutton.helpers.Utils +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.NotificationsAdapter +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.databinding.ItemStatusNotificationBinding +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.AbsoluteTimeFormatter +import com.keylesspalace.tusky.util.SmartLengthInputFilter +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.getRelativeTimeSpanString +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.setClickableText +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.viewdata.NotificationViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import java.util.Date + +internal class StatusNotificationViewHolder( + private val binding: ItemStatusNotificationBinding, + private val statusActionListener: StatusActionListener, + private val notificationActionListener: NotificationsAdapter.NotificationActionListener, + private val absoluteTimeFormatter: AbsoluteTimeFormatter +) : NotificationsViewHolder, RecyclerView.ViewHolder(binding.root) { + private val avatarRadius48dp = itemView.context.resources.getDimensionPixelSize( + R.dimen.avatar_radius_48dp + ) + private val avatarRadius36dp = itemView.context.resources.getDimensionPixelSize( + R.dimen.avatar_radius_36dp + ) + private val avatarRadius24dp = itemView.context.resources.getDimensionPixelSize( + R.dimen.avatar_radius_24dp + ) + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>?, + statusDisplayOptions: StatusDisplayOptions + ) { + val statusViewData = viewData.statusViewData + if (payloads.isNullOrEmpty()) { + /* in some very rare cases servers sends null status even though they should not */ + if (statusViewData == null) { + showNotificationContent(false) + } else { + showNotificationContent(true) + val (_, _, account, _, _, _, _, createdAt) = statusViewData.actionable + setDisplayName(account.name, account.emojis, statusDisplayOptions.animateEmojis) + setUsername(account.username) + setCreatedAt(createdAt, statusDisplayOptions.useAbsoluteTime) + if (viewData.type == Notification.Type.STATUS || + viewData.type == Notification.Type.UPDATE + ) { + setAvatar( + account.avatar, + account.bot, + statusDisplayOptions.animateAvatars, + statusDisplayOptions.showBotOverlay + ) + } else { + setAvatars( + account.avatar, + viewData.account.avatar, + statusDisplayOptions.animateAvatars + ) + } + + binding.notificationContainer.setOnClickListener { + // TODO + } + binding.notificationContent.setOnClickListener { + // TODO + } + binding.notificationTopText.setOnClickListener { + notificationActionListener.onViewAccount(viewData.account.id) + } + } + setMessage(viewData, statusActionListener, statusDisplayOptions.animateEmojis) + } else { + for (item in payloads) { + if (StatusBaseViewHolder.Key.KEY_CREATED == item && statusViewData != null) { + setCreatedAt( + statusViewData.status.actionableStatus.createdAt, + statusDisplayOptions.useAbsoluteTime + ) + } + } + } + } + + private fun showNotificationContent(show: Boolean) { + binding.statusDisplayName.visible(show) + binding.statusUsername.visible(show) + binding.statusMetaInfo.visible(show) + binding.notificationContentWarningDescription.visible(show) + binding.notificationContentWarningButton.visible(show) + binding.notificationContent.visible(show) + binding.notificationStatusAvatar.visible(show) + binding.notificationNotificationAvatar.visible(show) + } + + private fun setDisplayName(name: String, emojis: List?, animateEmojis: Boolean) { + val emojifiedName = name.emojify(emojis, binding.statusDisplayName, animateEmojis) + binding.statusDisplayName.text = emojifiedName + } + + private fun setUsername(name: String) { + val context = binding.statusUsername.context + val format = context.getString(R.string.post_username_format) + val usernameText = String.format(format, name) + binding.statusUsername.text = usernameText + } + + private fun setCreatedAt(createdAt: Date?, useAbsoluteTime: Boolean) { + if (useAbsoluteTime) { + binding.statusMetaInfo.text = absoluteTimeFormatter.format(createdAt, true) + } else { + val readout: String // visible timestamp + val readoutAloud: CharSequence // for screenreaders so they don't mispronounce timestamps like "17m" as 17 meters + if (createdAt != null) { + val then = createdAt.time + val now = System.currentTimeMillis() + readout = getRelativeTimeSpanString(binding.statusMetaInfo.context, then, now) + readoutAloud = DateUtils.getRelativeTimeSpanString( + then, + now, + DateUtils.SECOND_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE + ) + } else { + // unknown minutes~ + readout = "?m" + readoutAloud = "? minutes" + } + binding.statusMetaInfo.text = readout + binding.statusMetaInfo.contentDescription = readoutAloud + } + } + + private fun getIconWithColor( + context: Context, + @DrawableRes drawable: Int, + @ColorRes color: Int + ): Drawable? { + val icon = ContextCompat.getDrawable(context, drawable) + icon?.setColorFilter(context.getColor(color), PorterDuff.Mode.SRC_ATOP) + return icon + } + + private fun setAvatar(statusAvatarUrl: String?, isBot: Boolean, animateAvatars: Boolean, showBotOverlay: Boolean) { + binding.notificationStatusAvatar.setPaddingRelative(0, 0, 0, 0) + loadAvatar( + statusAvatarUrl, + binding.notificationStatusAvatar, + avatarRadius48dp, + animateAvatars + ) + if (showBotOverlay && isBot) { + binding.notificationNotificationAvatar.visibility = View.VISIBLE + Glide.with(binding.notificationNotificationAvatar) + .load(R.drawable.bot_badge) + .into(binding.notificationNotificationAvatar) + } else { + binding.notificationNotificationAvatar.visibility = View.GONE + } + } + + private fun setAvatars(statusAvatarUrl: String?, notificationAvatarUrl: String?, animateAvatars: Boolean) { + val padding = Utils.dpToPx(binding.notificationStatusAvatar.context, 12) + binding.notificationStatusAvatar.setPaddingRelative(0, 0, padding, padding) + loadAvatar( + statusAvatarUrl, + binding.notificationStatusAvatar, + avatarRadius36dp, + animateAvatars + ) + binding.notificationNotificationAvatar.visibility = View.VISIBLE + loadAvatar( + notificationAvatarUrl, + binding.notificationNotificationAvatar, + avatarRadius24dp, + animateAvatars + ) + } + + fun setMessage( + notificationViewData: NotificationViewData.Concrete, + listener: LinkListener, + animateEmojis: Boolean + ) { + val statusViewData = notificationViewData.statusViewData + val displayName = notificationViewData.account.name.unicodeWrap() + val type = notificationViewData.type + val context = binding.notificationTopText.context + val format: String + val icon: Drawable? + when (type) { + Notification.Type.FAVOURITE -> { + icon = getIconWithColor(context, R.drawable.ic_star_24dp, R.color.tusky_orange) + format = context.getString(R.string.notification_favourite_format) + } + Notification.Type.REBLOG -> { + icon = getIconWithColor(context, R.drawable.ic_repeat_24dp, R.color.tusky_blue) + format = context.getString(R.string.notification_reblog_format) + } + Notification.Type.STATUS -> { + icon = getIconWithColor(context, R.drawable.ic_home_24dp, R.color.tusky_blue) + format = context.getString(R.string.notification_subscription_format) + } + Notification.Type.UPDATE -> { + icon = getIconWithColor(context, R.drawable.ic_edit_24dp, R.color.tusky_blue) + format = context.getString(R.string.notification_update_format) + } + else -> { + icon = getIconWithColor(context, R.drawable.ic_star_24dp, R.color.tusky_orange) + format = context.getString(R.string.notification_favourite_format) + } + } + binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds( + icon, + null, + null, + null + ) + val wholeMessage = String.format(format, displayName) + val str = SpannableStringBuilder(wholeMessage) + val displayNameIndex = format.indexOf("%s") + str.setSpan( + StyleSpan(Typeface.BOLD), + displayNameIndex, + displayNameIndex + displayName.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + val emojifiedText = str.emojify( + notificationViewData.account.emojis, + binding.notificationTopText, + animateEmojis + ) + binding.notificationTopText.text = emojifiedText + if (statusViewData != null) { + val hasSpoiler = !TextUtils.isEmpty(statusViewData.status.spoilerText) + binding.notificationContentWarningDescription.visibility = + if (hasSpoiler) View.VISIBLE else View.GONE + binding.notificationContentWarningButton.visibility = + if (hasSpoiler) View.VISIBLE else View.GONE + if (statusViewData.isExpanded) { + binding.notificationContentWarningButton.setText( + R.string.post_content_warning_show_less + ) + } else { + binding.notificationContentWarningButton.setText( + R.string.post_content_warning_show_more + ) + } + binding.notificationContentWarningButton.setOnClickListener { + if (bindingAdapterPosition != RecyclerView.NO_POSITION) { + notificationActionListener.onExpandedChange( + !statusViewData.isExpanded, + bindingAdapterPosition + ) + } + binding.notificationContent.visibility = + if (statusViewData.isExpanded) View.GONE else View.VISIBLE + } + setupContentAndSpoiler(listener, statusViewData, animateEmojis) + } + } + + private fun setupContentAndSpoiler( + listener: LinkListener, + statusViewData: StatusViewData.Concrete, + animateEmojis: Boolean + ) { + val shouldShowContentIfSpoiler = statusViewData.isExpanded + val hasSpoiler = !TextUtils.isEmpty(statusViewData.status.spoilerText) + if (!shouldShowContentIfSpoiler && hasSpoiler) { + binding.notificationContent.visibility = View.GONE + } else { + binding.notificationContent.visibility = View.VISIBLE + } + val content = statusViewData.content + val emojis = statusViewData.actionable.emojis + if (statusViewData.isCollapsible && (statusViewData.isExpanded || !hasSpoiler)) { + binding.buttonToggleNotificationContent.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + notificationActionListener.onNotificationContentCollapsedChange( + !statusViewData.isCollapsed, + position + ) + } + } + binding.buttonToggleNotificationContent.visibility = View.VISIBLE + if (statusViewData.isCollapsed) { + binding.buttonToggleNotificationContent.setText( + R.string.post_content_warning_show_more + ) + binding.notificationContent.filters = COLLAPSE_INPUT_FILTER + } else { + binding.buttonToggleNotificationContent.setText( + R.string.post_content_warning_show_less + ) + binding.notificationContent.filters = NO_INPUT_FILTER + } + } else { + binding.buttonToggleNotificationContent.visibility = View.GONE + binding.notificationContent.filters = NO_INPUT_FILTER + } + val emojifiedText = + content.emojify( + emojis, + binding.notificationContent, + animateEmojis + ) + setClickableText( + binding.notificationContent, + emojifiedText, + statusViewData.actionable.mentions, + statusViewData.actionable.tags, + listener + ) + val emojifiedContentWarning: CharSequence = statusViewData.status.spoilerText.emojify( + statusViewData.actionable.emojis, + binding.notificationContentWarningDescription, + animateEmojis + ) + binding.notificationContentWarningDescription.text = emojifiedContentWarning + } + + companion object { + private val COLLAPSE_INPUT_FILTER: Array = arrayOf(SmartLengthInputFilter) + private val NO_INPUT_FILTER: Array = arrayOf() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt new file mode 100644 index 0000000000..9494b49303 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . + */ + +package com.keylesspalace.tusky.components.notifications + +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.databinding.ItemStatusBinding +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.NotificationViewData + +internal class StatusViewHolder( + binding: ItemStatusBinding, + private val statusActionListener: StatusActionListener, + private val accountId: String +) : NotificationsViewHolder, StatusViewHolder(binding.root) { + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>?, + statusDisplayOptions: StatusDisplayOptions + ) { + val statusViewData = viewData.statusViewData + if (statusViewData == null) { + /* in some very rare cases servers sends null status even though they should not */ + showStatusContent(false) + } else { + if (payloads.isNullOrEmpty()) { + showStatusContent(true) + } + setupWithStatus( + statusViewData, + statusActionListener, + statusDisplayOptions, + payloads?.firstOrNull() + ) + } + if (viewData.type == Notification.Type.POLL) { + setPollInfo(accountId == viewData.account.id) + } else { + hideStatusInfo() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/UnknownNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/UnknownNotificationViewHolder.kt new file mode 100644 index 0000000000..cf86095476 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/UnknownNotificationViewHolder.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . + */ + +package com.keylesspalace.tusky.components.notifications + +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.databinding.ItemStatusBinding +import com.keylesspalace.tusky.databinding.ItemUnknownNotificationBinding +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.NotificationViewData + +internal class UnknownNotificationViewHolder( + binding: ItemUnknownNotificationBinding, +) : NotificationsViewHolder, StatusViewHolder(binding.root) { + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>?, + statusDisplayOptions: StatusDisplayOptions + ) { + // nothing to do + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt index a232989b3a..df4c751509 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt @@ -24,6 +24,7 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.PlaceholderViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.util.StatusDisplayOptions @@ -53,7 +54,7 @@ class TimelinePagingAdapter( StatusViewHolder(inflater.inflate(R.layout.item_status_wrapper, viewGroup, false)) } VIEW_TYPE_PLACEHOLDER -> { - PlaceholderViewHolder(inflater.inflate(R.layout.item_status_placeholder, viewGroup, false)) + PlaceholderViewHolder(ItemStatusPlaceholderBinding.inflate(inflater, viewGroup, false), statusListener) } else -> { StatusViewHolder(inflater.inflate(R.layout.item_status, viewGroup, false)) @@ -81,7 +82,7 @@ class TimelinePagingAdapter( val status = getItem(position) if (status is StatusViewData.Placeholder) { val holder = viewHolder as PlaceholderViewHolder - holder.setup(statusListener, status.isLoading) + holder.setup(status.isLoading) } else if (status is StatusViewData.Concrete) { val holder = viewHolder as StatusViewHolder holder.setupWithStatus( diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 413d282b50..220194c592 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -93,7 +93,7 @@ import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.view.EndlessOnScrollListener; import com.keylesspalace.tusky.viewdata.AttachmentViewData; -import com.keylesspalace.tusky.viewdata.NotificationViewData; +import com.keylesspalace.tusky.viewdata.NotificationViewData2; import com.keylesspalace.tusky.viewdata.StatusViewData; import java.io.IOException; @@ -178,10 +178,10 @@ private Placeholder(long id) { private boolean showingError; // Each element is either a Notification for loading data or a Placeholder - private final PairedList, NotificationViewData> notifications + private final PairedList, NotificationViewData2> notifications = new PairedList<>(new Function<>() { @Override - public NotificationViewData apply(Either input) { + public NotificationViewData2 apply(Either input) { if (input.isRight()) { Notification notification = input.asRight() .rewriteToStatusTypeIfNeeded(accountManager.getActiveAccount().getAccountId()); @@ -195,7 +195,7 @@ public NotificationViewData apply(Either input) { true ); } else { - return new NotificationViewData.Placeholder(input.asLeft().id, false); + return new NotificationViewData2.Placeholder(input.asLeft().id, false); } } }); @@ -236,10 +236,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c binding.recyclerView.setLayoutManager(layoutManager); binding.recyclerView.setAccessibilityDelegateCompat( new ListStatusAccessibilityDelegate(binding.recyclerView, this, (pos) -> { - NotificationViewData notification = notifications.getPairedItemOrNull(pos); + NotificationViewData2 notification = notifications.getPairedItemOrNull(pos); // We support replies only for now - if (notification instanceof NotificationViewData.Concrete) { - return ((NotificationViewData.Concrete) notification).getStatusViewData(); + if (notification instanceof NotificationViewData2.Concrete) { + return ((NotificationViewData2.Concrete) notification).getStatusViewData(); } else { return null; } @@ -557,8 +557,8 @@ public void onLoadMore(int position) { } sendFetchNotificationsRequest(previous.getId(), next.getId(), FetchEnd.MIDDLE, position); Placeholder placeholder = notifications.get(position).asLeft(); - NotificationViewData notificationViewData = - new NotificationViewData.Placeholder(placeholder.id, true); + NotificationViewData2 notificationViewData = + new NotificationViewData2.Placeholder(placeholder.id, true); notifications.setPairedItem(position, notificationViewData); updateAdapter(); } else { @@ -595,14 +595,14 @@ private void updateStatus(String statusId, Function mapper) { // 4. update notificationViewData Status oldStatus = notifications.get(index).asRight().getStatus(); - NotificationViewData.Concrete oldViewData = - (NotificationViewData.Concrete) this.notifications.getPairedItem(index); + NotificationViewData2.Concrete oldViewData = + (NotificationViewData2.Concrete) this.notifications.getPairedItem(index); Status newStatus = mapper.apply(oldStatus); Notification newNotification = this.notifications.get(index).asRight() .copyWithStatus(newStatus); StatusViewData.Concrete newStatusViewData = Objects.requireNonNull(oldViewData.getStatusViewData()).copyWithStatus(newStatus); - NotificationViewData.Concrete newViewData = oldViewData.copyWithStatus(newStatusViewData); + NotificationViewData2.Concrete newViewData = oldViewData.copyWithStatus(newStatusViewData); notifications.set(index, new Either.Right<>(newNotification)); notifications.setPairedItem(index, newViewData); @@ -622,15 +622,15 @@ private void updateViewDataAt(int position, Log.e(TAG, message); return; } - NotificationViewData someViewData = this.notifications.getPairedItem(position); - if (!(someViewData instanceof NotificationViewData.Concrete)) { + NotificationViewData2 someViewData = this.notifications.getPairedItem(position); + if (!(someViewData instanceof NotificationViewData2.Concrete)) { return; } - NotificationViewData.Concrete oldViewData = (NotificationViewData.Concrete) someViewData; + NotificationViewData2.Concrete oldViewData = (NotificationViewData2.Concrete) someViewData; StatusViewData.Concrete oldStatusViewData = oldViewData.getStatusViewData(); if (oldStatusViewData == null) return; - NotificationViewData.Concrete newViewData = + NotificationViewData2.Concrete newViewData = oldViewData.copyWithStatus(mapper.apply(oldStatusViewData)); notifications.setPairedItem(position, newViewData); @@ -894,8 +894,8 @@ private void onLoadMore() { if (last.isRight()) { final Placeholder placeholder = newPlaceholder(); notifications.add(new Either.Left<>(placeholder)); - NotificationViewData viewData = - new NotificationViewData.Placeholder(placeholder.id, true); + NotificationViewData2 viewData = + new NotificationViewData2.Placeholder(placeholder.id, true); notifications.setPairedItem(notifications.size() - 1, viewData); updateAdapter(); } @@ -1011,8 +1011,8 @@ private void onFetchNotificationsFailure(Throwable throwable, FetchEnd fetchEnd, binding.swipeRefreshLayout.setRefreshing(false); if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) { Placeholder placeholder = notifications.get(position).asLeft(); - NotificationViewData placeholderVD = - new NotificationViewData.Placeholder(placeholder.id, false); + NotificationViewData2 placeholderVD = + new NotificationViewData2.Placeholder(placeholder.id, false); notifications.setPairedItem(position, placeholderVD); updateAdapter(); } else if (this.notifications.isEmpty()) { @@ -1204,11 +1204,11 @@ public void onChanged(int position, int count, Object payload) { } }; - private final AsyncListDiffer + private final AsyncListDiffer differ = new AsyncListDiffer<>(listUpdateCallback, new AsyncDifferConfig.Builder<>(diffCallback).build()); - private final NotificationsAdapter.AdapterDataSource dataSource = + private final NotificationsAdapter.AdapterDataSource dataSource = new NotificationsAdapter.AdapterDataSource<>() { @Override public int getItemCount() { @@ -1216,27 +1216,27 @@ public int getItemCount() { } @Override - public NotificationViewData getItemAt(int pos) { + public NotificationViewData2 getItemAt(int pos) { return differ.getCurrentList().get(pos); } }; - private static final DiffUtil.ItemCallback diffCallback + private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback<>() { @Override - public boolean areItemsTheSame(NotificationViewData oldItem, NotificationViewData newItem) { + public boolean areItemsTheSame(NotificationViewData2 oldItem, NotificationViewData2 newItem) { return oldItem.getViewDataId() == newItem.getViewDataId(); } @Override - public boolean areContentsTheSame(@NonNull NotificationViewData oldItem, @NonNull NotificationViewData newItem) { + public boolean areContentsTheSame(@NonNull NotificationViewData2 oldItem, @NonNull NotificationViewData2 newItem) { return false; } @Nullable @Override - public Object getChangePayload(@NonNull NotificationViewData oldItem, @NonNull NotificationViewData newItem) { + public Object getChangePayload(@NonNull NotificationViewData2 oldItem, @NonNull NotificationViewData2 newItem) { if (oldItem.deepEquals(newItem)) { // If items are equal - update timestamp only return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED); diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt index 5c1f3ebc27..228f4dec7e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -36,7 +36,7 @@ package com.keylesspalace.tusky.util import com.keylesspalace.tusky.entity.Notification import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TrendingTag -import com.keylesspalace.tusky.viewdata.NotificationViewData +import com.keylesspalace.tusky.viewdata.NotificationViewData2 import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.TrendingViewData @@ -60,8 +60,8 @@ fun Notification.toViewData( isShowingContent: Boolean, isExpanded: Boolean, isCollapsed: Boolean -): NotificationViewData.Concrete { - return NotificationViewData.Concrete( +): NotificationViewData2.Concrete { + return NotificationViewData2.Concrete( this.type, this.id, this.account, diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt index c70e2fc71e..f54651e379 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt @@ -1,4 +1,4 @@ -/* Copyright 2017 Andrew Dawson +/* Copyright 2023 Tusky Contributors * * This file is a part of Tusky. * @@ -12,127 +12,26 @@ * * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ +package com.keylesspalace.tusky.viewdata -package com.keylesspalace.tusky.viewdata; +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.Report +import com.keylesspalace.tusky.entity.TimelineAccount -import androidx.annotation.Nullable; +sealed class NotificationViewData { -import com.keylesspalace.tusky.entity.Notification; -import com.keylesspalace.tusky.entity.Report; -import com.keylesspalace.tusky.entity.TimelineAccount; + abstract val id: String -import java.util.Objects; + class Concrete( + override val id: String, + val type: Notification.Type, + val account: TimelineAccount, + val statusViewData: StatusViewData.Concrete?, + val report: Report? + ) : NotificationViewData() -/** - * Created by charlag on 12/07/2017. - *

- * Class to represent data required to display either a notification or a placeholder. - * It is either a {@link Placeholder} or a {@link Concrete}. - * It is modelled this way because close relationship between placeholder and concrete notification - * is fine in this case. Placeholder case is not modelled as a type of notification because - * invariants would be violated and because it would model domain incorrectly. It is preferable to - * {@link com.keylesspalace.tusky.util.Either} because class hierarchy is cheaper, faster and - * more native. - */ -public abstract class NotificationViewData { - private NotificationViewData() { - } - - public abstract long getViewDataId(); - - public abstract boolean deepEquals(NotificationViewData other); - - public static final class Concrete extends NotificationViewData { - private final Notification.Type type; - private final String id; - private final TimelineAccount account; - @Nullable - private final StatusViewData.Concrete statusViewData; - @Nullable - private final Report report; - - public Concrete(Notification.Type type, String id, TimelineAccount account, - @Nullable StatusViewData.Concrete statusViewData, @Nullable Report report) { - this.type = type; - this.id = id; - this.account = account; - this.statusViewData = statusViewData; - this.report = report; - } - - public Notification.Type getType() { - return type; - } - - public String getId() { - return id; - } - - public TimelineAccount getAccount() { - return account; - } - - @Nullable - public StatusViewData.Concrete getStatusViewData() { - return statusViewData; - } - - @Nullable - public Report getReport() { - return report; - } - - @Override - public long getViewDataId() { - return id.hashCode(); - } - - @Override - public boolean deepEquals(NotificationViewData o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Concrete concrete = (Concrete) o; - return type == concrete.type && - Objects.equals(id, concrete.id) && - account.getId().equals(concrete.account.getId()) && - (Objects.equals(statusViewData, concrete.statusViewData)) && - (Objects.equals(report, concrete.report)); - } - - @Override - public int hashCode() { - - return Objects.hash(type, id, account, statusViewData); - } - - public Concrete copyWithStatus(@Nullable StatusViewData.Concrete statusViewData) { - return new Concrete(type, id, account, statusViewData, report); - } - } - - public static final class Placeholder extends NotificationViewData { - private final long id; - private final boolean isLoading; - - public Placeholder(long id, boolean isLoading) { - this.id = id; - this.isLoading = isLoading; - } - - public boolean isLoading() { - return isLoading; - } - - @Override - public long getViewDataId() { - return id; - } - - @Override - public boolean deepEquals(NotificationViewData other) { - if (!(other instanceof Placeholder)) return false; - Placeholder that = (Placeholder) other; - return isLoading == that.isLoading && id == that.id; - } - } + class Placeholder( + override val id: String, + val isLoading: Boolean + ) : NotificationViewData() } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData2.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData2.java new file mode 100644 index 0000000000..2825500c07 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData2.java @@ -0,0 +1,138 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.viewdata; + +import androidx.annotation.Nullable; + +import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.entity.Report; +import com.keylesspalace.tusky.entity.TimelineAccount; + +import java.util.Objects; + +/** + * Created by charlag on 12/07/2017. + *

+ * Class to represent data required to display either a notification or a placeholder. + * It is either a {@link Placeholder} or a {@link Concrete}. + * It is modelled this way because close relationship between placeholder and concrete notification + * is fine in this case. Placeholder case is not modelled as a type of notification because + * invariants would be violated and because it would model domain incorrectly. It is preferable to + * {@link com.keylesspalace.tusky.util.Either} because class hierarchy is cheaper, faster and + * more native. + */ +public abstract class NotificationViewData2 { + private NotificationViewData2() { + } + + public abstract long getViewDataId(); + + public abstract boolean deepEquals(NotificationViewData2 other); + + public static final class Concrete extends NotificationViewData2 { + private final Notification.Type type; + private final String id; + private final TimelineAccount account; + @Nullable + private final StatusViewData.Concrete statusViewData; + @Nullable + private final Report report; + + public Concrete(Notification.Type type, String id, TimelineAccount account, + @Nullable StatusViewData.Concrete statusViewData, @Nullable Report report) { + this.type = type; + this.id = id; + this.account = account; + this.statusViewData = statusViewData; + this.report = report; + } + + public Notification.Type getType() { + return type; + } + + public String getId() { + return id; + } + + public TimelineAccount getAccount() { + return account; + } + + @Nullable + public StatusViewData.Concrete getStatusViewData() { + return statusViewData; + } + + @Nullable + public Report getReport() { + return report; + } + + @Override + public long getViewDataId() { + return id.hashCode(); + } + + @Override + public boolean deepEquals(NotificationViewData2 o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Concrete concrete = (Concrete) o; + return type == concrete.type && + Objects.equals(id, concrete.id) && + account.getId().equals(concrete.account.getId()) && + (Objects.equals(statusViewData, concrete.statusViewData)) && + (Objects.equals(report, concrete.report)); + } + + @Override + public int hashCode() { + + return Objects.hash(type, id, account, statusViewData); + } + + public Concrete copyWithStatus(@Nullable StatusViewData.Concrete statusViewData) { + return new Concrete(type, id, account, statusViewData, report); + } + } + + public static final class Placeholder extends NotificationViewData2 { + private final long id; + private final boolean isLoading; + + public Placeholder(long id, boolean isLoading) { + this.id = id; + this.isLoading = isLoading; + } + + public boolean isLoading() { + return isLoading; + } + + @Override + public long getViewDataId() { + return id; + } + + @Override + public boolean deepEquals(NotificationViewData2 other) { + if (!(other instanceof Placeholder)) return false; + Placeholder that = (Placeholder) other; + return isLoading == that.isLoading && id == that.id; + } + } +} diff --git a/app/src/main/res/layout/item_status_placeholder.xml b/app/src/main/res/layout/item_status_placeholder.xml index 660c99eaa4..4159e6babb 100644 --- a/app/src/main/res/layout/item_status_placeholder.xml +++ b/app/src/main/res/layout/item_status_placeholder.xml @@ -1,51 +1,24 @@ - - - + android:background="@color/dividerColorOther"> - + android:textSize="?attr/status_text_large" + android:textStyle="bold" /> + + + diff --git a/app/src/main/res/layout/item_unknown_notification.xml b/app/src/main/res/layout/item_unknown_notification.xml new file mode 100644 index 0000000000..09afceb3f6 --- /dev/null +++ b/app/src/main/res/layout/item_unknown_notification.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 90c41c9706..7fb4d4c860 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -767,6 +767,7 @@ Rule violation Spam + Legal Other Unfollow #%s? @@ -843,4 +844,5 @@ Delete filter \'%1$s\'?" Delete Do you want to save your profile changes? + Unknown notification type diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3c6d9810a7..8e25651bd3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -133,12 +133,6 @@ 0 - -