fix: make feed notifications robust against subscription changes

This commit is contained in:
Bnyro
2025-05-07 20:58:12 +02:00
parent 54cf71740d
commit c8c29ac64f
6 changed files with 38 additions and 36 deletions

View File

@ -140,11 +140,13 @@ object PreferenceKeys {
// Internally saved data / not a preference // Internally saved data / not a preference
const val ERROR_LOG = "error_log" const val ERROR_LOG = "error_log"
const val LAST_STREAM_VIDEO_ID = "last_stream_video_id"
const val LAST_WATCHED_FEED_TIME = "last_watched_feed_time"
const val AUTH_PREF_FILE = "auth" const val AUTH_PREF_FILE = "auth"
const val IMAGE_PROXY_URL = "image_proxy_url" const val IMAGE_PROXY_URL = "image_proxy_url"
const val SELECTED_CHANNEL_GROUP = "selected_channel_group" const val SELECTED_CHANNEL_GROUP = "selected_channel_group"
const val SELECTED_DOWNLOAD_SORT_TYPE = "selected_download_sort_type" const val SELECTED_DOWNLOAD_SORT_TYPE = "selected_download_sort_type"
const val LAST_SHOWN_INFO_MESSAGE_VERSION_CODE = "last_shown_info_message_version" const val LAST_SHOWN_INFO_MESSAGE_VERSION_CODE = "last_shown_info_message_version"
// use the helper methods at PreferenceHelper to access these
const val LAST_USER_SEEN_FEED_TIME = "last_watched_feed_time"
const val LAST_REFRESHED_FEED_TIME = "last_refreshed_feed_time"
} }

View File

@ -96,24 +96,24 @@ object PreferenceHelper {
authSettings.edit { putString(PreferenceKeys.USERNAME, newValue) } authSettings.edit { putString(PreferenceKeys.USERNAME, newValue) }
} }
fun setLastSeenVideoId(videoId: String) { fun updateLastFeedWatchedTime(time: Long, seenByUser: Boolean) {
putString(PreferenceKeys.LAST_STREAM_VIDEO_ID, videoId)
}
fun getLastSeenVideoId(): String {
return getString(PreferenceKeys.LAST_STREAM_VIDEO_ID, "")
}
fun updateLastFeedWatchedTime(time: Long) {
// only update the time if the time is newer // only update the time if the time is newer
// this avoids cases, where the user last saw an older video, which had already been seen, // this avoids cases, where the user last saw an older video, which had already been seen,
// causing all following video to be incorrectly marked as unseen again // causing all following video to be incorrectly marked as unseen again
if (getLastCheckedFeedTime() < time) if (getLastCheckedFeedTime(false) < time)
putLong(PreferenceKeys.LAST_WATCHED_FEED_TIME, time) putLong(PreferenceKeys.LAST_REFRESHED_FEED_TIME, time)
// this value holds the last time the user opened the subscriptions feed
// whereas [LAST_REFRESHED_FEED_TIME] considers the last time the feed was loaded,
// which could also be possible in the background (e.g. via notifications)
if (seenByUser && getLastCheckedFeedTime(true) < time)
putLong(PreferenceKeys.LAST_USER_SEEN_FEED_TIME, time)
} }
fun getLastCheckedFeedTime(): Long { fun getLastCheckedFeedTime(seenByUser: Boolean): Long {
return getLong(PreferenceKeys.LAST_WATCHED_FEED_TIME, 0) val key =
if (seenByUser) PreferenceKeys.LAST_USER_SEEN_FEED_TIME else PreferenceKeys.LAST_REFRESHED_FEED_TIME
return getLong(key, 0)
} }
fun saveErrorLog(log: String) { fun saveErrorLog(log: String) {

View File

@ -37,7 +37,6 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.ActivityMainBinding import com.github.libretube.databinding.ActivityMainBinding
import com.github.libretube.enums.ImportFormat import com.github.libretube.enums.ImportFormat
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImportHelper import com.github.libretube.helpers.ImportHelper
import com.github.libretube.helpers.IntentHelper import com.github.libretube.helpers.IntentHelper
import com.github.libretube.helpers.NavBarHelper import com.github.libretube.helpers.NavBarHelper
@ -240,9 +239,12 @@ class MainActivity : BaseActivity() {
subscriptionsViewModel.fetchSubscriptions(this) subscriptionsViewModel.fetchSubscriptions(this)
subscriptionsViewModel.videoFeed.observe(this) { feed -> subscriptionsViewModel.videoFeed.observe(this) { feed ->
val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime(seenByUser = true)
val lastSeenVideoIndex = feed.orEmpty() val lastSeenVideoIndex = feed.orEmpty()
.indexOfFirst { PreferenceHelper.getLastSeenVideoId() == it.url?.toID() } .filter { !it.isUpcoming }
.indexOfFirst { it.uploaded <= lastCheckedFeedTime }
if (lastSeenVideoIndex < 1) return@observe if (lastSeenVideoIndex < 1) return@observe
binding.bottomNav.getOrCreateBadge(R.id.subscriptionsFragment).apply { binding.bottomNav.getOrCreateBadge(R.id.subscriptionsFragment).apply {
number = lastSeenVideoIndex number = lastSeenVideoIndex
backgroundColor = ThemeHelper.getThemeColor( backgroundColor = ThemeHelper.getThemeColor(

View File

@ -140,13 +140,17 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
// any other feed updates are caused by manual refreshing and thus should reset the scroll // any other feed updates are caused by manual refreshing and thus should reset the scroll
// position to zero // position to zero
var alreadyShowedFeedOnce = false var alreadyShowedFeedOnce = false
viewModel.videoFeed.observe(viewLifecycleOwner) { viewModel.videoFeed.observe(viewLifecycleOwner) { feed ->
if (!viewModel.isCurrentTabSubChannels && it != null) { if (!viewModel.isCurrentTabSubChannels && feed != null) {
lifecycleScope.launch { lifecycleScope.launch {
showFeed(!alreadyShowedFeedOnce) showFeed(!alreadyShowedFeedOnce)
} }
alreadyShowedFeedOnce = true alreadyShowedFeedOnce = true
} }
feed?.firstOrNull { !it.isUpcoming }?.uploaded?.let {
PreferenceHelper.updateLastFeedWatchedTime(it, true)
}
} }
// restore the scroll position, same conditions as above // restore the scroll position, same conditions as above
@ -384,7 +388,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
// add an "all caught up item" // add an "all caught up item"
if (selectedSortOrder == 0) { if (selectedSortOrder == 0) {
val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime() val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime(seenByUser = true)
val caughtUpIndex = val caughtUpIndex =
feed.indexOfFirst { it.uploaded <= lastCheckedFeedTime && !it.isUpcoming } feed.indexOfFirst { it.uploaded <= lastCheckedFeedTime && !it.isUpcoming }
if (caughtUpIndex > 0 && !feed[caughtUpIndex - 1].isUpcoming) { if (caughtUpIndex > 0 && !feed[caughtUpIndex - 1].isUpcoming) {
@ -404,10 +408,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
binding.toggleSubs.text = getString(R.string.subscriptions) binding.toggleSubs.text = getString(R.string.subscriptions)
feed.firstOrNull { !it.isUpcoming }?.uploaded?.let {
PreferenceHelper.updateLastFeedWatchedTime(it)
}
binding.subRefresh.isRefreshing = false binding.subRefresh.isRefreshing = false
feedAdapter.submitList(sortedFeed) { feedAdapter.submitList(sortedFeed) {

View File

@ -11,7 +11,6 @@ import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Subscription import com.github.libretube.api.obj.Subscription
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.repo.FeedProgress import com.github.libretube.repo.FeedProgress
@ -41,9 +40,8 @@ class SubscriptionsViewModel : ViewModel() {
return@launch return@launch
} }
this@SubscriptionsViewModel.videoFeed.postValue(videoFeed) this@SubscriptionsViewModel.videoFeed.postValue(videoFeed)
if (videoFeed.isNotEmpty()) { videoFeed.firstOrNull { !it.isUpcoming }?.uploaded?.let {
// save the last recent video to the prefs for the notification worker PreferenceHelper.updateLastFeedWatchedTime(it, false)
PreferenceHelper.setLastSeenVideoId(videoFeed[0].url!!.toID())
} }
} }
} }

View File

@ -83,18 +83,15 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
val videoFeed = try { val videoFeed = try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
SubscriptionHelper.getFeed(forceRefresh = true) SubscriptionHelper.getFeed(forceRefresh = true)
} }.filter { !it.isUpcoming }
} catch (e: Exception) { } catch (e: Exception) {
return false return false
} }
val lastUserSeenVideoId = PreferenceHelper.getLastSeenVideoId() val lastFeedCheckMillis = PreferenceHelper.getLastCheckedFeedTime(seenByUser = false)
val mostRecentStreamId = videoFeed.firstOrNull()?.url?.toID() ?: return true
// save the latest streams that got notified about
PreferenceHelper.setLastSeenVideoId(mostRecentStreamId)
// first time notifications are enabled or no new video available // first time notifications are enabled or no new video available
if (lastUserSeenVideoId.isEmpty() || lastUserSeenVideoId == mostRecentStreamId) return true if (lastFeedCheckMillis == 0L || videoFeed.none { it.uploaded > lastFeedCheckMillis }) return true
val channelsToIgnore = PreferenceHelper.getIgnorableNotificationChannels() val channelsToIgnore = PreferenceHelper.getIgnorableNotificationChannels()
val enableShortsNotification = val enableShortsNotification =
@ -102,7 +99,7 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
val channelGroups = videoFeed.asSequence() val channelGroups = videoFeed.asSequence()
// filter the new videos until the last seen video in the feed // filter the new videos until the last seen video in the feed
.takeWhile { it.url!!.toID() != lastUserSeenVideoId } .filter { it.uploaded > lastFeedCheckMillis }
// don't show notifications for shorts videos if not enabled // don't show notifications for shorts videos if not enabled
.filter { enableShortsNotification || !it.isShort } .filter { enableShortsNotification || !it.isShort }
// hide for notifications unsubscribed channels // hide for notifications unsubscribed channels
@ -110,6 +107,9 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
// group the new streams by the uploader // group the new streams by the uploader
.groupBy { it.uploaderUrl!!.toID() } .groupBy { it.uploaderUrl!!.toID() }
// update the last feed check time in order to not show the same notification again
PreferenceHelper.updateLastFeedWatchedTime(videoFeed.first().uploaded, seenByUser = false)
// return if the previous video didn't get found or all the channels have notifications disabled // return if the previous video didn't get found or all the channels have notifications disabled
if (channelGroups.isEmpty()) return true if (channelGroups.isEmpty()) return true
@ -226,6 +226,6 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
companion object { companion object {
private const val INTENT_FLAGS = Intent.FLAG_ACTIVITY_CLEAR_TOP or private const val INTENT_FLAGS = Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
} }