Refactor notifications to Kotlin & paging (#4026)

This refactors the NotificationsFragment and related classes to Kotlin &
paging.
While trying to preserve as much of the original behavior as possible,
this adds the following improvements as well:
- The "show notifications filter" preference was added again
- The "load more" button now has a background ripple effect when clicked
- The "legal" report category of Mastodon 4.2 is now supported in report
notifications
- Unknown notifications now display "unknown notification type" instead
of an empty line

Other code quality improvements:
- All views from xml layouts are now referenced via ViewBindings
- the classes responsible for showing system notifications were moved to
a new package `systemnotifications` while the classes from this
refactoring are in `notifications`
- the id of the local Tusky account is now called `tuskyAccountId` in
all places I could find

closes https://github.com/tuskyapp/Tusky/issues/3429

---------

Co-authored-by: Zongle Wang <wangzongler@gmail.com>
This commit is contained in:
Konrad Pozniak 2024-05-03 18:27:10 +02:00 committed by GitHub
commit b2c0b18c8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
121 changed files with 6992 additions and 4654 deletions

View file

@ -1,65 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.util
import kotlin.time.Duration
import kotlin.time.TimeMark
import kotlin.time.TimeSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/**
* Returns a flow that mirrors the original flow, but filters out values that occur within
* [timeout] of the previously emitted value. The first value is always emitted.
*
* Example:
*
* ```kotlin
* flow {
* emit(1)
* delay(90.milliseconds)
* emit(2)
* delay(90.milliseconds)
* emit(3)
* delay(1010.milliseconds)
* emit(4)
* delay(1010.milliseconds)
* emit(5)
* }.throttleFirst(1000.milliseconds)
* ```
*
* produces the following emissions.
*
* ```text
* 1, 4, 5
* ```
*
* @see kotlinx.coroutines.flow.debounce(Duration)
* @param timeout Emissions within this duration of the last emission are filtered
* @param timeSource Used to measure elapsed time. Normally only overridden in tests
*/
fun <T> Flow<T>.throttleFirst(timeout: Duration, timeSource: TimeSource = TimeSource.Monotonic) =
flow {
var marker: TimeMark? = null
collect {
if (marker == null || marker!!.elapsedNow() >= timeout) {
emit(it)
marker = timeSource.markNow()
}
}
}

View file

@ -20,13 +20,6 @@ package com.keylesspalace.tusky.util
import java.util.ArrayList
import java.util.LinkedHashSet
/**
* @return true if list is null or else return list.isEmpty()
*/
fun isEmpty(list: List<*>?): Boolean {
return list == null || list.isEmpty()
}
/**
* @return a new ArrayList containing the elements without duplicates in the same order
*/

View file

@ -18,7 +18,7 @@ package com.keylesspalace.tusky.util
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.entity.AccountEntity
import java.util.Locale
private const val TAG: String = "LocaleUtils"

View file

@ -1,74 +0,0 @@
package com.keylesspalace.tusky.util
import androidx.arch.core.util.Function
/**
* This list implementation can help to keep two lists in sync - like real models and view models.
*
* Every operation on the main list triggers update of the supplementary list (but not vice versa).
*
* This makes sure that the main list is always the source of truth.
*
* Main list is projected to the supplementary list by the passed mapper function.
*
* Paired list is newer actually exposed and clients are provided with `getPairedCopy()`,
* `getPairedItem()` and `setPairedItem()`. This prevents modifications of the
* supplementary list size so lists are always have the same length.
*
* This implementation will not try to recover from exceptional cases so lists may be out of sync
* after the exception.
*
* It is most useful with immutable data because we cannot track changes inside stored objects.
*
* @param T type of elements in the main list
* @param V type of elements in supplementary list
* @param mapper Function, which will be used to translate items from the main list to the
* supplementary one.
* @constructor
*/
class PairedList<T, V>(private val mapper: Function<T, out V>) : AbstractMutableList<T>() {
private val main: MutableList<T> = ArrayList()
private val synced: MutableList<V> = ArrayList()
val pairedCopy: List<V>
get() = ArrayList(synced)
fun getPairedItem(index: Int): V {
return synced[index]
}
fun getPairedItemOrNull(index: Int): V? {
return synced.getOrNull(index)
}
fun setPairedItem(index: Int, element: V) {
synced[index] = element
}
override fun get(index: Int): T {
return main[index]
}
override fun set(index: Int, element: T): T {
synced[index] = mapper.apply(element)
return main.set(index, element)
}
override fun add(element: T): Boolean {
synced.add(mapper.apply(element))
return main.add(element)
}
override fun add(index: Int, element: T) {
synced.add(index, mapper.apply(element))
main.add(index, element)
}
override fun removeAt(index: Int): T {
synced.removeAt(index)
return main.removeAt(index)
}
override val size: Int
get() = main.size
}

View file

@ -28,8 +28,8 @@ import androidx.core.graphics.drawable.IconCompat
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.di.ApplicationScope
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope

View file

@ -1,29 +0,0 @@
package com.keylesspalace.tusky.util
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import at.connyduck.calladapter.networkresult.NetworkResult
import at.connyduck.calladapter.networkresult.fold
import java.util.function.Consumer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
/**
* Simple reimplementation of RxJava's Single using a Kotlin coroutine,
* intended to be consumed by legacy Java code only.
*/
class Single<T>(private val producer: suspend CoroutineScope.() -> NetworkResult<T>) {
fun subscribe(
owner: LifecycleOwner,
onSuccess: Consumer<T>,
onError: Consumer<Throwable>
): Job {
return owner.lifecycleScope.launch {
producer().fold(
onSuccess = { onSuccess.accept(it) },
onFailure = { onError.accept(it) }
)
}
}
}

View file

@ -18,7 +18,7 @@
package com.keylesspalace.tusky.util
import android.content.SharedPreferences
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.settings.PrefKeys
data class StatusDisplayOptions(

View file

@ -36,10 +36,8 @@ package com.keylesspalace.tusky.util
import androidx.paging.CombinedLoadStates
import androidx.paging.LoadState
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.StatusViewData
import com.keylesspalace.tusky.viewdata.TranslationViewData
import com.keylesspalace.tusky.viewdata.TrendingViewData
@ -61,21 +59,6 @@ fun Status.toViewData(
)
}
@JvmName("notificationToViewData")
fun Notification.toViewData(
isShowingContent: Boolean,
isExpanded: Boolean,
isCollapsed: Boolean
): NotificationViewData.Concrete {
return NotificationViewData.Concrete(
this.type,
this.id,
this.account,
this.status?.toViewData(isShowingContent, isExpanded, isCollapsed),
this.report
)
}
fun List<TrendingTag>.toViewData(): List<TrendingViewData.Tag> {
val maxTrendingValue = flatMap { tag -> tag.history }
.mapNotNull { it.uses.toLongOrNull() }