Merge pull request #1663 from nrobi144/phase1-final
Desktop - Threads and Notifications + Empty state updates
This commit is contained in:
+2
-2
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.verify
|
||||
@@ -174,7 +174,7 @@ class ThreadDualAxisChartAssemblerTest {
|
||||
null,
|
||||
)
|
||||
|
||||
val filter = ThreadFeedFilter(account, naddr.toTag())
|
||||
val filter = ThreadFeedFilter(account, naddr.toTag(), LocalCache)
|
||||
val calculatedFeed = filter.feed()
|
||||
|
||||
val expectedOrder =
|
||||
|
||||
@@ -1662,7 +1662,7 @@ class Account(
|
||||
|
||||
fun isHidden(userHex: String): Boolean = hiddenUsers.flow.value.isUserHidden(userHex)
|
||||
|
||||
fun followingKeySet(): Set<HexKey> = kind3FollowList.flow.value.authors
|
||||
override fun followingKeySet(): Set<HexKey> = kind3FollowList.flow.value.authors
|
||||
|
||||
fun isAcceptable(user: User): Boolean {
|
||||
if (userProfile().pubkeyHex == user.pubkeyHex) {
|
||||
|
||||
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.model
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.IChannel
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
||||
@@ -212,7 +214,7 @@ interface ILocalCache {
|
||||
) {}
|
||||
}
|
||||
|
||||
object LocalCache : ILocalCache {
|
||||
object LocalCache : ILocalCache, ICacheProvider {
|
||||
val antiSpam = AntiSpamFilter()
|
||||
|
||||
val users = LargeSoftCache<HexKey, User>()
|
||||
@@ -317,16 +319,35 @@ object LocalCache : ILocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun getUserIfExists(key: String): User? {
|
||||
override fun getUserIfExists(key: String): User? {
|
||||
if (key.isEmpty()) return null
|
||||
return users.get(key)
|
||||
}
|
||||
|
||||
override fun countUsers(predicate: (String, Any) -> Boolean): Int {
|
||||
var count = 0
|
||||
users.forEach { key, user ->
|
||||
if (predicate(key, user)) count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
override fun getAnyChannel(note: Any?): IChannel? {
|
||||
val channelNote = note as? Note ?: return null
|
||||
val channel = getAnyChannel(channelNote)
|
||||
// Wrap Channel to implement IChannel interface
|
||||
return channel?.let {
|
||||
object : IChannel {
|
||||
override fun relays(): List<Any>? = it.relays().toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAddressableNoteIfExists(key: String): AddressableNote? = Address.parse(key)?.let { addressables.get(it) }
|
||||
|
||||
fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address)
|
||||
|
||||
fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) }
|
||||
override fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) }
|
||||
|
||||
fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId)
|
||||
|
||||
@@ -357,7 +378,7 @@ object LocalCache : ILocalCache {
|
||||
return null
|
||||
}
|
||||
|
||||
fun checkGetOrCreateNote(key: String): Note? {
|
||||
override fun checkGetOrCreateNote(key: String): Note? {
|
||||
if (ATag.isATag(key)) {
|
||||
return checkGetOrCreateAddressableNote(key)
|
||||
}
|
||||
@@ -382,6 +403,19 @@ object LocalCache : ILocalCache {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getEventStream(): com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream =
|
||||
object : com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream {
|
||||
override val newEventBundles = live.newEventBundles
|
||||
override val deletedEventBundles = live.deletedEventBundles
|
||||
}
|
||||
|
||||
override fun hasBeenDeleted(event: Any): Boolean =
|
||||
if (event is Event) {
|
||||
deletionIndex.hasBeenDeleted(event)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
fun getOrAddAliasNote(
|
||||
idHex: String,
|
||||
note: Note,
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
class ThreadAssembler {
|
||||
private fun searchRoot(
|
||||
note: Note,
|
||||
testedNotes: MutableSet<Note> = mutableSetOf(),
|
||||
): Note? {
|
||||
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
|
||||
|
||||
val noteEvent = note.event
|
||||
|
||||
if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) return note
|
||||
|
||||
testedNotes.add(note)
|
||||
|
||||
val markedAsRoot =
|
||||
noteEvent
|
||||
?.tags
|
||||
?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }
|
||||
?.getOrNull(1)
|
||||
if (markedAsRoot != null) {
|
||||
// Check to ssee if there is an error in the tag and the root has replies
|
||||
if (LocalCache.getNoteIfExists(markedAsRoot)?.replyTo?.isEmpty() == true) {
|
||||
return LocalCache.checkGetOrCreateNote(markedAsRoot)
|
||||
}
|
||||
}
|
||||
|
||||
val hasNoReplyTo =
|
||||
note.replyTo?.lastOrNull {
|
||||
it.replyTo?.isEmpty() == true
|
||||
}
|
||||
if (hasNoReplyTo != null) return hasNoReplyTo
|
||||
|
||||
// recursive
|
||||
val roots =
|
||||
note.replyTo?.mapNotNull {
|
||||
if (it !in testedNotes) {
|
||||
searchRoot(it, testedNotes)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (roots != null && roots.isNotEmpty()) {
|
||||
return roots[0]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@Stable
|
||||
class ThreadInfo(
|
||||
val root: Note,
|
||||
val allNotes: ImmutableSet<Note>,
|
||||
)
|
||||
|
||||
fun findRoot(noteId: String): Note? {
|
||||
val note = LocalCache.checkGetOrCreateNote(noteId) ?: return null
|
||||
|
||||
return if (note.event != null) {
|
||||
val thread = OnlyLatestVersionSet()
|
||||
|
||||
searchRoot(note, thread) ?: note
|
||||
} else {
|
||||
note
|
||||
}
|
||||
}
|
||||
|
||||
fun findThreadFor(noteId: String): ThreadInfo? {
|
||||
checkNotInMainThread()
|
||||
|
||||
val note = LocalCache.checkGetOrCreateNote(noteId) ?: return null
|
||||
|
||||
return if (note.event != null) {
|
||||
val thread = OnlyLatestVersionSet()
|
||||
|
||||
val threadRoot = searchRoot(note, thread) ?: note
|
||||
|
||||
loadUp(note, thread)
|
||||
|
||||
loadDown(threadRoot, thread)
|
||||
// adds the replies of the note in case the search for Root
|
||||
// did not added them.
|
||||
note.replies.forEach { loadDown(it, thread) }
|
||||
|
||||
ThreadInfo(
|
||||
root = note,
|
||||
allNotes = thread.toImmutableSet(),
|
||||
)
|
||||
} else {
|
||||
ThreadInfo(
|
||||
root = note,
|
||||
allNotes = setOf(note).toImmutableSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadUp(
|
||||
note: Note,
|
||||
thread: MutableSet<Note>,
|
||||
) {
|
||||
if (note !in thread) {
|
||||
thread.add(note)
|
||||
|
||||
note.replyTo?.forEach { loadUp(it, thread) }
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDown(
|
||||
note: Note,
|
||||
thread: MutableSet<Note>,
|
||||
) {
|
||||
if (note !in thread) {
|
||||
thread.add(note)
|
||||
|
||||
note.replies.forEach { loadDown(it, thread) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OnlyLatestVersionSet : MutableSet<Note> {
|
||||
val map = hashMapOf<Address, Long>()
|
||||
val set = hashSetOf<Note>()
|
||||
|
||||
override fun add(element: Note): Boolean {
|
||||
val loadedCreatedAt = element.createdAt()
|
||||
val noteEvent = element.event
|
||||
|
||||
return if (element is AddressableNote && loadedCreatedAt != null) {
|
||||
innerAdd(element.address, element, loadedCreatedAt)
|
||||
} else if (noteEvent is AddressableEvent && loadedCreatedAt != null) {
|
||||
innerAdd(noteEvent.address(), element, loadedCreatedAt)
|
||||
} else {
|
||||
set.add(element)
|
||||
}
|
||||
}
|
||||
|
||||
private fun innerAdd(
|
||||
address: Address,
|
||||
element: Note,
|
||||
loadedCreatedAt: Long,
|
||||
): Boolean {
|
||||
val existing = map.get(address)
|
||||
return if (existing == null) {
|
||||
map.put(address, loadedCreatedAt)
|
||||
set.add(element)
|
||||
} else {
|
||||
if (loadedCreatedAt > existing) {
|
||||
map.put(address, loadedCreatedAt)
|
||||
set.add(element)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<Note>): Boolean = elements.map { add(it) }.any()
|
||||
|
||||
override val size: Int
|
||||
get() = set.size
|
||||
|
||||
override fun clear() {
|
||||
set.clear()
|
||||
map.clear()
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean = set.isEmpty()
|
||||
|
||||
override fun containsAll(elements: Collection<Note>): Boolean = set.containsAll(elements)
|
||||
|
||||
override fun contains(element: Note): Boolean = set.contains(element)
|
||||
|
||||
override fun iterator(): MutableIterator<Note> = set.iterator()
|
||||
|
||||
override fun retainAll(elements: Collection<Note>): Boolean = set.retainAll(elements)
|
||||
|
||||
override fun removeAll(elements: Collection<Note>): Boolean = elements.map { remove(it) }.any()
|
||||
|
||||
override fun remove(element: Note): Boolean {
|
||||
element.address()?.let {
|
||||
map.remove(it)
|
||||
}
|
||||
(element.event as? AddressableEvent)?.address()?.let {
|
||||
map.remove(it)
|
||||
}
|
||||
|
||||
return set.remove(element)
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import java.lang.Long.min
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
data class LevelSignature(
|
||||
val signature: String,
|
||||
val createdAt: Long?,
|
||||
val author: User?,
|
||||
)
|
||||
|
||||
object ThreadLevelCalculator {
|
||||
val levelFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd-HH:mm:ss")
|
||||
|
||||
private fun formattedDateTime(timestamp: Long): String =
|
||||
Instant
|
||||
.ofEpochSecond(timestamp)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.format(levelFormatter)
|
||||
|
||||
/**
|
||||
* This method caches signatures during each execution to avoid recalculation in longer threads
|
||||
*/
|
||||
fun replyLevelSignature(
|
||||
note: Note,
|
||||
eventsToConsider: Set<HexKey>,
|
||||
cachedSignatures: MutableMap<Note, LevelSignature>,
|
||||
account: User,
|
||||
accountFollowingSet: Set<String>,
|
||||
now: Long,
|
||||
): LevelSignature {
|
||||
val replyTo = note.replyTo
|
||||
|
||||
// estimates the min date by replies if it doesn't exist.
|
||||
val createdAt =
|
||||
note.createdAt() ?: min(
|
||||
note.replies.minOfOrNull { it.createdAt() ?: now } ?: now,
|
||||
note.reactions.values.minOfOrNull { it.minOfOrNull { it.createdAt() ?: now } ?: now } ?: now,
|
||||
)
|
||||
|
||||
val noteAuthor = note.author
|
||||
|
||||
if (
|
||||
note.event is RepostEvent || note.event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()
|
||||
) {
|
||||
return LevelSignature(
|
||||
signature = "/" + formattedDateTime(createdAt) + note.idHex.substring(0, 8) + ";",
|
||||
createdAt = createdAt,
|
||||
author = noteAuthor,
|
||||
)
|
||||
}
|
||||
|
||||
val parent =
|
||||
(
|
||||
replyTo
|
||||
.filter {
|
||||
it.idHex in eventsToConsider
|
||||
} // This forces the signature to be based on a branch, avoiding two roots
|
||||
.map {
|
||||
cachedSignatures[it]
|
||||
?: replyLevelSignature(
|
||||
it,
|
||||
eventsToConsider,
|
||||
cachedSignatures,
|
||||
account,
|
||||
accountFollowingSet,
|
||||
now,
|
||||
).apply { cachedSignatures.put(it, this) }
|
||||
}.maxByOrNull { it.signature.length }
|
||||
)
|
||||
|
||||
val parentSignature = parent?.signature?.removeSuffix(";") ?: ""
|
||||
|
||||
val threadOrder =
|
||||
if (noteAuthor != null && parent?.author == noteAuthor) {
|
||||
// author of the thread first, in **ascending** order
|
||||
"9" + formattedDateTime((parent.createdAt ?: 0) + (now - createdAt)) + note.idHex.substring(0, 8)
|
||||
} else if (noteAuthor != null && noteAuthor.pubkeyHex == account.pubkeyHex) {
|
||||
"8" + formattedDateTime(createdAt) + note.idHex.substring(0, 8) // my replies
|
||||
} else if (noteAuthor != null && noteAuthor.pubkeyHex in accountFollowingSet) {
|
||||
"7" + formattedDateTime(createdAt) + note.idHex.substring(0, 8) // my follows replies.
|
||||
} else {
|
||||
"0" + formattedDateTime(createdAt) + note.idHex.substring(0, 8) // everyone else.
|
||||
}
|
||||
|
||||
val mySignature =
|
||||
LevelSignature(
|
||||
signature = "$parentSignature/$threadOrder;",
|
||||
createdAt = createdAt,
|
||||
author = note.author,
|
||||
)
|
||||
|
||||
cachedSignatures[note] = mySignature
|
||||
return mySignature
|
||||
}
|
||||
|
||||
fun replyLevel(
|
||||
note: Note,
|
||||
cachedLevels: MutableMap<Note, Int> = mutableMapOf(),
|
||||
): Int {
|
||||
val replyTo = note.replyTo
|
||||
if (
|
||||
note.event is RepostEvent || note.event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()
|
||||
) {
|
||||
cachedLevels[note] = 0
|
||||
return 0
|
||||
}
|
||||
|
||||
val thisLevel =
|
||||
replyTo.maxOf {
|
||||
cachedLevels[it] ?: replyLevel(it, cachedLevels)
|
||||
} + 1
|
||||
|
||||
cachedLevels[note] = thisLevel
|
||||
|
||||
return thisLevel
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/** This class is designed to have a waiting time between two calls of invalidate */
|
||||
class BundledUpdate(
|
||||
val delay: Long,
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) {
|
||||
// Exists to avoid exceptions stopping the coroutine
|
||||
val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e("BundledUpdate", "Caught exception: ${throwable.message}", throwable)
|
||||
}
|
||||
|
||||
val scope = CoroutineScope(dispatcher + SupervisorJob() + exceptionHandler)
|
||||
|
||||
val bundledUpdate = BasicBundledUpdate(delay, dispatcher, scope)
|
||||
|
||||
fun invalidate(
|
||||
ignoreIfDoing: Boolean = false,
|
||||
onUpdate: suspend () -> Unit,
|
||||
) = bundledUpdate.invalidate(ignoreIfDoing, onUpdate)
|
||||
|
||||
fun cancel() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/** This class is designed to have a waiting time between two calls of invalidate */
|
||||
class BasicBundledUpdate(
|
||||
val delay: Long,
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
private var onlyOneInBlock = AtomicBoolean()
|
||||
private var invalidatesAgain = false
|
||||
|
||||
fun invalidate(
|
||||
ignoreIfDoing: Boolean = false,
|
||||
onUpdate: suspend () -> Unit,
|
||||
) {
|
||||
if (onlyOneInBlock.getAndSet(true)) {
|
||||
if (!ignoreIfDoing) {
|
||||
invalidatesAgain = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch(dispatcher) {
|
||||
try {
|
||||
onUpdate()
|
||||
delay(delay)
|
||||
if (invalidatesAgain) {
|
||||
onUpdate()
|
||||
}
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
invalidatesAgain = false
|
||||
onlyOneInBlock.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** This class is designed to have a waiting time between two calls of invalidate */
|
||||
class BundledInsert<T>(
|
||||
val delay: Long,
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) {
|
||||
// Exists to avoid exceptions stopping the coroutine
|
||||
val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e("BundledInsert", "Caught exception: ${throwable.message}", throwable)
|
||||
}
|
||||
|
||||
val scope = CoroutineScope(dispatcher + SupervisorJob() + exceptionHandler)
|
||||
|
||||
val bundledInsert = BasicBundledInsert<T>(delay, dispatcher, scope)
|
||||
|
||||
fun invalidateList(
|
||||
newObject: T,
|
||||
onUpdate: suspend (Set<T>) -> Unit,
|
||||
) = bundledInsert.invalidateList(newObject, onUpdate)
|
||||
|
||||
fun cancel() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/** This class is designed to have a waiting time between two calls of invalidate */
|
||||
class BasicBundledInsert<T>(
|
||||
val delay: Long,
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
private var onlyOneInBlock = AtomicBoolean()
|
||||
private var queue = LinkedBlockingQueue<T>()
|
||||
|
||||
fun invalidateList(
|
||||
newObject: T,
|
||||
onUpdate: suspend (Set<T>) -> Unit,
|
||||
) {
|
||||
queue.put(newObject)
|
||||
|
||||
if (onlyOneInBlock.getAndSet(true)) {
|
||||
// if it was true already, returns.
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch(dispatcher) {
|
||||
try {
|
||||
while (true) {
|
||||
val batch = mutableSetOf<T>()
|
||||
queue.drainTo(batch)
|
||||
if (batch.isNotEmpty()) {
|
||||
onUpdate(batch)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
delay(delay)
|
||||
}
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
onlyOneInBlock.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-13
@@ -18,17 +18,10 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
interface IFeedFilter<T> {
|
||||
fun loadTop(): List<T>
|
||||
|
||||
fun limit(): Int = 500
|
||||
|
||||
/** Returns a string that serves as the key to invalidate the list if it changes. */
|
||||
fun feedKey(): Any
|
||||
|
||||
fun showHiddenKey(): Boolean = false
|
||||
|
||||
fun feed(): List<T>
|
||||
}
|
||||
// Re-export from commons for backwards compatibility
|
||||
typealias BundledUpdate = com.vitorpamplona.amethyst.commons.service.BundledUpdate
|
||||
typealias BasicBundledUpdate = com.vitorpamplona.amethyst.commons.service.BasicBundledUpdate
|
||||
typealias BundledInsert<T> = com.vitorpamplona.amethyst.commons.service.BundledInsert<T>
|
||||
typealias BasicBundledInsert<T> = com.vitorpamplona.amethyst.commons.service.BasicBundledInsert<T>
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
|
||||
abstract class AdditiveFeedFilter<T> :
|
||||
FeedFilter<T>(),
|
||||
IAdditiveFeedFilter<T> {
|
||||
open fun updateListWith(
|
||||
oldList: List<T>,
|
||||
newItems: Set<T>,
|
||||
): List<T> =
|
||||
logTime(
|
||||
debugMessage = { "${this.javaClass.simpleName} AdditiveFeedFilter updating ${newItems.size} new items to ${it.size} items" },
|
||||
) {
|
||||
val newItemsToBeAdded = applyFilter(newItems)
|
||||
if (newItemsToBeAdded.isNotEmpty()) {
|
||||
val newList = oldList.toSet() + newItemsToBeAdded
|
||||
sort(newList).take(limit())
|
||||
} else {
|
||||
oldList
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-12
@@ -20,15 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
|
||||
abstract class FeedFilter<T> : IFeedFilter<T> {
|
||||
override fun loadTop(): List<T> {
|
||||
val feed =
|
||||
logTime(
|
||||
debugMessage = { "${this.javaClass.simpleName} FeedFilter returning ${it.size} objects" },
|
||||
block = ::feed,
|
||||
)
|
||||
return feed.take(limit())
|
||||
}
|
||||
}
|
||||
// Re-export from commons for backwards compatibility
|
||||
typealias IFeedFilter<T> = com.vitorpamplona.amethyst.commons.ui.feeds.IFeedFilter<T>
|
||||
typealias IAdditiveFeedFilter<T> = com.vitorpamplona.amethyst.commons.ui.feeds.IAdditiveFeedFilter<T>
|
||||
typealias FeedFilter<T> = com.vitorpamplona.amethyst.commons.ui.feeds.FeedFilter<T>
|
||||
typealias AdditiveFeedFilter<T> = com.vitorpamplona.amethyst.commons.ui.feeds.AdditiveFeedFilter<T>
|
||||
@@ -1,231 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.feeds
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.BasicBundledInsert
|
||||
import com.vitorpamplona.amethyst.service.BasicBundledUpdate
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.IFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.utils.flattenToSet
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
class FeedContentState(
|
||||
val localFilter: IFeedFilter<Note>,
|
||||
val viewModelScope: CoroutineScope,
|
||||
) : InvalidatableContent {
|
||||
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
// Simple counter that changes when it needs to invalidate everything
|
||||
private val _scrollToTop = MutableStateFlow<Int>(0)
|
||||
val scrollToTop = _scrollToTop.asStateFlow()
|
||||
var scrollToTopPending = false
|
||||
|
||||
private var lastFeedKey: Any? = null
|
||||
|
||||
override val isRefreshing: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
val lastNoteCreatedAtWhenFullyLoaded = MutableStateFlow<Long?>(null)
|
||||
|
||||
fun sendToTop() {
|
||||
if (scrollToTopPending) return
|
||||
|
||||
scrollToTopPending = true
|
||||
viewModelScope.launch(Dispatchers.IO) { _scrollToTop.emit(_scrollToTop.value + 1) }
|
||||
}
|
||||
|
||||
suspend fun sentToTop() {
|
||||
scrollToTopPending = false
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
viewModelScope.launch(Dispatchers.IO) { refreshSuspended() }
|
||||
}
|
||||
|
||||
fun visibleNotes(): List<Note> {
|
||||
val currentState = _feedContent.value
|
||||
return if (currentState is FeedState.Loaded) {
|
||||
currentState.feed.value.list
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun lastNoteCreatedAtIfFilled() = lastNoteCreatedAtWhenFullyLoaded.value
|
||||
|
||||
fun refreshSuspended() {
|
||||
checkNotInMainThread()
|
||||
|
||||
isRefreshing.value = true
|
||||
try {
|
||||
lastFeedKey = localFilter.feedKey()
|
||||
val notes = localFilter.loadTop().distinctBy { it.idHex }.toImmutableList()
|
||||
|
||||
val oldNotesState = _feedContent.value
|
||||
if (oldNotesState is FeedState.Loaded) {
|
||||
if (!equalImmutableLists(notes, oldNotesState.feed.value.list)) {
|
||||
updateFeed(notes)
|
||||
}
|
||||
} else {
|
||||
updateFeed(notes)
|
||||
}
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFeed(notes: ImmutableList<Note>) {
|
||||
if (notes.size >= localFilter.limit()) {
|
||||
// feeds might not be sorted by created at, so full search
|
||||
val lastNoteTime = notes.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE }
|
||||
if (lastNoteTime != lastNoteCreatedAtWhenFullyLoaded.value) {
|
||||
lastNoteCreatedAtWhenFullyLoaded.tryEmit(lastNoteTime)
|
||||
}
|
||||
} else {
|
||||
lastNoteCreatedAtWhenFullyLoaded.tryEmit(null)
|
||||
}
|
||||
|
||||
val currentState = _feedContent.value
|
||||
if (notes.isEmpty()) {
|
||||
_feedContent.tryEmit(FeedState.Empty)
|
||||
} else if (currentState is FeedState.Loaded) {
|
||||
currentState.feed.tryEmit(LoadedFeedState(notes, localFilter.showHiddenKey()))
|
||||
} else {
|
||||
_feedContent.tryEmit(
|
||||
FeedState.Loaded(MutableStateFlow(LoadedFeedState(notes, localFilter.showHiddenKey()))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFromFeed(deletedNotes: Set<Note>) {
|
||||
val feed = _feedContent.value
|
||||
if (feed is FeedState.Loaded) {
|
||||
val notes = (feed.feed.value.list - deletedNotes).toImmutableList()
|
||||
updateFeed(notes)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshFromOldState(newItems: Set<Note>) {
|
||||
val oldNotesState = _feedContent.value
|
||||
if (localFilter is AdditiveFeedFilter && lastFeedKey == localFilter.feedKey()) {
|
||||
if (oldNotesState is FeedState.Loaded) {
|
||||
val deletionEvents: List<DeletionEvent> = newItems.mapNotNull { it.event as? DeletionEvent }
|
||||
|
||||
val oldList =
|
||||
if (deletionEvents.isEmpty()) {
|
||||
oldNotesState.feed.value.list
|
||||
} else {
|
||||
oldNotesState.feed.value.list
|
||||
.filter {
|
||||
val noteEvent = it.event
|
||||
if (noteEvent != null) {
|
||||
!LocalCache.deletionIndex.hasBeenDeleted(noteEvent)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
val newList =
|
||||
localFilter
|
||||
.updateListWith(oldList, newItems)
|
||||
.distinctBy { it.idHex }
|
||||
.toImmutableList()
|
||||
|
||||
if (!equalImmutableLists(newList, oldNotesState.feed.value.list)) {
|
||||
updateFeed(newList)
|
||||
}
|
||||
} else if (oldNotesState is FeedState.Empty) {
|
||||
val newList =
|
||||
localFilter
|
||||
.updateListWith(emptyList(), newItems)
|
||||
.distinctBy { it.idHex }
|
||||
.toImmutableList()
|
||||
if (newList.isNotEmpty()) {
|
||||
updateFeed(newList)
|
||||
}
|
||||
} else {
|
||||
// Refresh Everything
|
||||
refreshSuspended()
|
||||
}
|
||||
} else {
|
||||
// Refresh Everything
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
|
||||
private val bundler = BasicBundledUpdate(250, Dispatchers.IO, viewModelScope)
|
||||
private val bundlerInsert = BasicBundledInsert<Set<Note>>(250, Dispatchers.IO, viewModelScope)
|
||||
|
||||
override fun invalidateData(ignoreIfDoing: Boolean) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
bundler.invalidate(ignoreIfDoing) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun checkKeysInvalidateDataAndSendToTop() {
|
||||
if (lastFeedKey != localFilter.feedKey()) {
|
||||
bundler.invalidate(false) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
refreshSuspended()
|
||||
sendToTop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun invalidateInsertData(newItems: Set<Note>) {
|
||||
bundlerInsert.invalidateList(newItems) {
|
||||
refreshFromOldState(it.flattenToSet())
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFeedWith(newNotes: Set<Note>) {
|
||||
if (
|
||||
localFilter is AdditiveFeedFilter &&
|
||||
(_feedContent.value is FeedState.Loaded || _feedContent.value is FeedState.Empty)
|
||||
) {
|
||||
invalidateInsertData(newNotes)
|
||||
} else {
|
||||
// Refresh Everything
|
||||
invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.feeds
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Stable
|
||||
sealed class FeedState {
|
||||
object Loading : FeedState()
|
||||
|
||||
class Loaded(
|
||||
val feed: MutableStateFlow<LoadedFeedState<Note>>,
|
||||
) : FeedState()
|
||||
|
||||
object Empty : FeedState()
|
||||
|
||||
class FeedError(
|
||||
val errorMessage: String,
|
||||
) : FeedState()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class LoadedFeedState<T>(
|
||||
val list: ImmutableList<T>,
|
||||
val showHidden: Boolean,
|
||||
)
|
||||
+9
-6
@@ -20,10 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.feeds
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
// Re-export from commons for backwards compatibility - import everything
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState as CommonsFeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState as CommonsFeedState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent as CommonsInvalidatableContent
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadedFeedState as CommonsLoadedFeedState
|
||||
|
||||
interface InvalidatableContent {
|
||||
fun invalidateData(ignoreIfDoing: Boolean = false)
|
||||
|
||||
val isRefreshing: State<Boolean>
|
||||
}
|
||||
typealias FeedState = CommonsFeedState
|
||||
typealias LoadedFeedState<T> = CommonsLoadedFeedState<T>
|
||||
typealias InvalidatableContent = CommonsInvalidatableContent
|
||||
typealias FeedContentState = CommonsFeedContentState
|
||||
@@ -28,12 +28,12 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedLoaded
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
||||
|
||||
@@ -20,52 +20,17 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
abstract class FeedViewModel(
|
||||
// Re-export from commons for backwards compatibility
|
||||
typealias FeedViewModel = com.vitorpamplona.amethyst.commons.viewmodels.FeedViewModel
|
||||
|
||||
/**
|
||||
* Android-specific FeedViewModel base class that provides LocalCache as the cache provider.
|
||||
* Subclasses can extend this to automatically use LocalCache without passing cacheProvider.
|
||||
*/
|
||||
abstract class AndroidFeedViewModel(
|
||||
localFilter: FeedFilter<Note>,
|
||||
) : ViewModel(),
|
||||
InvalidatableContent {
|
||||
val feedState = FeedContentState(localFilter, viewModelScope)
|
||||
|
||||
override val isRefreshing = feedState.isRefreshing
|
||||
|
||||
fun sendToTop() = feedState.sendToTop()
|
||||
|
||||
suspend fun sentToTop() = feedState.sentToTop()
|
||||
|
||||
override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing)
|
||||
|
||||
init {
|
||||
Log.d("Init", "Starting new Model: ${this.javaClass.simpleName}")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
Log.d("Rendering Metrics", "Update feeds: ${this@FeedViewModel.javaClass.simpleName} with ${newNotes.size}")
|
||||
feedState.updateFeedWith(newNotes)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.deletedEventBundles.collect { newNotes ->
|
||||
Log.d("Rendering Metrics", "Delete from feeds: ${this@FeedViewModel.javaClass.simpleName} with ${newNotes.size}")
|
||||
feedState.deleteFromFeed(newNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
) : FeedViewModel(localFilter, LocalCache)
|
||||
|
||||
@@ -25,12 +25,12 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
+15
-14
@@ -20,11 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListKnownFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListNewFeedFilter
|
||||
@@ -50,28 +51,28 @@ class AccountFeedContentStates(
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
val homeLive = ChannelFeedContentState(HomeLiveFilter(account), scope)
|
||||
val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(account), scope)
|
||||
val homeReplies = FeedContentState(HomeConversationsFeedFilter(account), scope)
|
||||
val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(account), scope, LocalCache)
|
||||
val homeReplies = FeedContentState(HomeConversationsFeedFilter(account), scope, LocalCache)
|
||||
|
||||
val dmKnown = FeedContentState(ChatroomListKnownFeedFilter(account), scope)
|
||||
val dmNew = FeedContentState(ChatroomListNewFeedFilter(account), scope)
|
||||
val dmKnown = FeedContentState(ChatroomListKnownFeedFilter(account), scope, LocalCache)
|
||||
val dmNew = FeedContentState(ChatroomListNewFeedFilter(account), scope, LocalCache)
|
||||
|
||||
val videoFeed = FeedContentState(VideoFeedFilter(account), scope)
|
||||
val videoFeed = FeedContentState(VideoFeedFilter(account), scope, LocalCache)
|
||||
|
||||
val discoverFollowSets = FeedContentState(DiscoverFollowSetsFeedFilter(account), scope)
|
||||
val discoverReads = FeedContentState(DiscoverLongFormFeedFilter(account), scope)
|
||||
val discoverMarketplace = FeedContentState(DiscoverMarketplaceFeedFilter(account), scope)
|
||||
val discoverDVMs = FeedContentState(DiscoverNIP89FeedFilter(account), scope)
|
||||
val discoverLive = FeedContentState(DiscoverLiveFeedFilter(account), scope)
|
||||
val discoverCommunities = FeedContentState(DiscoverCommunityFeedFilter(account), scope)
|
||||
val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope)
|
||||
val discoverFollowSets = FeedContentState(DiscoverFollowSetsFeedFilter(account), scope, LocalCache)
|
||||
val discoverReads = FeedContentState(DiscoverLongFormFeedFilter(account), scope, LocalCache)
|
||||
val discoverMarketplace = FeedContentState(DiscoverMarketplaceFeedFilter(account), scope, LocalCache)
|
||||
val discoverDVMs = FeedContentState(DiscoverNIP89FeedFilter(account), scope, LocalCache)
|
||||
val discoverLive = FeedContentState(DiscoverLiveFeedFilter(account), scope, LocalCache)
|
||||
val discoverCommunities = FeedContentState(DiscoverCommunityFeedFilter(account), scope, LocalCache)
|
||||
val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope, LocalCache)
|
||||
|
||||
val notifications = CardFeedContentState(NotificationFeedFilter(account), scope)
|
||||
val notificationSummary = NotificationSummaryState(account)
|
||||
|
||||
val feedListOptions = TopNavFilterState(account, scope)
|
||||
|
||||
val drafts = FeedContentState(DraftEventsFeedFilter(account), scope)
|
||||
val drafts = FeedContentState(DraftEventsFeedFilter(account), scope, LocalCache)
|
||||
|
||||
suspend fun init() {
|
||||
notificationSummary.initializeSuspend()
|
||||
|
||||
+1
-1
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
|
||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync
|
||||
import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
@@ -70,7 +71,6 @@ import com.vitorpamplona.amethyst.ui.actions.Dao
|
||||
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||
import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
@Stable
|
||||
class BookmarkPrivateFeedViewModel(
|
||||
val account: Account,
|
||||
) : FeedViewModel(BookmarkPrivateFeedFilter(account)) {
|
||||
) : AndroidFeedViewModel(BookmarkPrivateFeedFilter(account)) {
|
||||
class Factory(
|
||||
val account: Account,
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
@Stable
|
||||
class BookmarkPublicFeedViewModel(
|
||||
val account: Account,
|
||||
) : FeedViewModel(BookmarkPublicFeedFilter(account)) {
|
||||
) : AndroidFeedViewModel(BookmarkPublicFeedFilter(account)) {
|
||||
class Factory(
|
||||
val account: Account,
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
+2
-2
@@ -30,12 +30,12 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
||||
|
||||
+4
-3
@@ -24,12 +24,13 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.ListChange
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -53,7 +54,7 @@ abstract class ListChangeFeedViewModel(
|
||||
localFilter: ChangesFlowFilter<Note>,
|
||||
) : ViewModel(),
|
||||
InvalidatableContent {
|
||||
val feedState = FeedContentState(localFilter, viewModelScope)
|
||||
val feedState = FeedContentState(localFilter, viewModelScope, LocalCache)
|
||||
|
||||
override val isRefreshing = feedState.isRefreshing
|
||||
|
||||
|
||||
+2
-2
@@ -31,11 +31,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
|
||||
+1
-1
@@ -36,8 +36,8 @@ import com.google.accompanist.adaptive.FoldAwareConfiguration
|
||||
import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy
|
||||
import com.google.accompanist.adaptive.TwoPane
|
||||
import com.google.accompanist.adaptive.calculateDisplayFeatures
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class CommunityFeedViewModel(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
) : FeedViewModel(CommunityFeedFilter(note, account)) {
|
||||
) : AndroidFeedViewModel(CommunityFeedFilter(note, account)) {
|
||||
class Factory(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class CommunityModerationFeedViewModel(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
) : FeedViewModel(CommunityModerationFeedFilter(note, account)) {
|
||||
) : AndroidFeedViewModel(CommunityModerationFeedFilter(note, account)) {
|
||||
class Factory(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
|
||||
+2
-2
@@ -55,11 +55,11 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
|
||||
+2
-2
@@ -46,9 +46,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS
|
||||
|
||||
+2
-2
@@ -24,14 +24,14 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
@Stable
|
||||
class NIP90ContentDiscoveryFeedViewModel(
|
||||
val account: Account,
|
||||
dvmKey: String,
|
||||
requestId: String,
|
||||
) : FeedViewModel(NIP90ContentDiscoveryResponseFilter(account, dvmKey, requestId)) {
|
||||
) : AndroidFeedViewModel(NIP90ContentDiscoveryResponseFilter(account, dvmKey, requestId)) {
|
||||
class Factory(
|
||||
val account: Account,
|
||||
val dvmKey: String,
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class FollowPackFeedConversationsFeedViewModel(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
) : FeedViewModel(FollowPackFeedConversationsFeedFilter(note, account)) {
|
||||
) : AndroidFeedViewModel(FollowPackFeedConversationsFeedFilter(note, account)) {
|
||||
class Factory(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class FollowPackFeedNewThreadFeedViewModel(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
) : FeedViewModel(FollowPackFeedNewThreadFeedFilter(note, account)) {
|
||||
) : AndroidFeedViewModel(FollowPackFeedNewThreadFeedFilter(note, account)) {
|
||||
class Factory(
|
||||
val note: AddressableNote,
|
||||
val account: Account,
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@Stable
|
||||
@@ -33,7 +33,7 @@ class GeoHashFeedViewModel(
|
||||
val geohash: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val account: Account,
|
||||
) : FeedViewModel(
|
||||
) : AndroidFeedViewModel(
|
||||
GeoHashFeedFilter(geohash, relays, account, LocalCache),
|
||||
) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@Stable
|
||||
@@ -33,7 +33,7 @@ class HashtagFeedViewModel(
|
||||
val hashtag: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val account: Account,
|
||||
) : FeedViewModel(
|
||||
) : AndroidFeedViewModel(
|
||||
HashtagFeedFilter(hashtag, relays, account, LocalCache),
|
||||
) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
||||
+2
-2
@@ -55,6 +55,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.model.AROUND_ME
|
||||
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
@@ -63,8 +65,6 @@ import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
|
||||
|
||||
+2
-16
@@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadedFeedState
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -35,8 +37,6 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrderCard
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
|
||||
@@ -460,20 +460,6 @@ class CardFeedContentState(
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> equalImmutableLists(
|
||||
list1: ImmutableList<T>,
|
||||
list2: ImmutableList<T>,
|
||||
): Boolean {
|
||||
if (list1 === list2) return true
|
||||
if (list1.size != list2.size) return false
|
||||
for (i in 0 until list1.size) {
|
||||
if (list1[i] !== list2[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class CombinedZap(
|
||||
val request: Note,
|
||||
|
||||
+1
-1
@@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadedFeedState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadedFeedState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
+9
-5
@@ -18,10 +18,14 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
||||
|
||||
interface IAdditiveFeedFilter<T> : IFeedFilter<T> {
|
||||
fun applyFilter(newItems: Set<T>): Set<T>
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
fun sort(items: Set<T>): List<T>
|
||||
}
|
||||
// Re-export from commons for backwards compatibility
|
||||
fun <T> equalImmutableLists(
|
||||
list1: ImmutableList<T>,
|
||||
list2: ImmutableList<T>,
|
||||
): Boolean =
|
||||
com.vitorpamplona.amethyst.commons.utils
|
||||
.equalImmutableLists(list1, list2)
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class UserProfileBookmarksFeedViewModel(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
) : FeedViewModel(UserProfileBookmarksFeedFilter(user, account)) {
|
||||
) : AndroidFeedViewModel(UserProfileBookmarksFeedFilter(user, account)) {
|
||||
class Factory(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class UserProfileConversationsFeedViewModel(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
) : FeedViewModel(UserProfileConversationsFeedFilter(user, account)) {
|
||||
) : AndroidFeedViewModel(UserProfileConversationsFeedFilter(user, account)) {
|
||||
class Factory(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
|
||||
+1
-1
@@ -33,10 +33,10 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class UserProfileGalleryFeedViewModel(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
) : FeedViewModel(UserProfileGalleryFeedFilter(user, account)) {
|
||||
) : AndroidFeedViewModel(UserProfileGalleryFeedFilter(user, account)) {
|
||||
class Factory(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
|
||||
+1
-1
@@ -34,8 +34,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
+2
-2
@@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class UserAppRecommendationsFeedViewModel(
|
||||
val user: User,
|
||||
) : FeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) {
|
||||
) : AndroidFeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) {
|
||||
class Factory(
|
||||
val user: User,
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class UserProfileMutualFeedViewModel(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
) : FeedViewModel(UserProfileMutualFeedFilter(user, account)) {
|
||||
) : AndroidFeedViewModel(UserProfileMutualFeedFilter(user, account)) {
|
||||
class Factory(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
|
||||
+2
-2
@@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class UserProfileNewThreadsFeedViewModel(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
) : FeedViewModel(UserProfileNewThreadFeedFilter(user, account)) {
|
||||
) : AndroidFeedViewModel(UserProfileNewThreadFeedFilter(user, account)) {
|
||||
class Factory(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
|
||||
+1
-1
@@ -25,9 +25,9 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.amethyst.model.RelayInfo
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
|
||||
+2
-2
@@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
class UserProfileReportFeedViewModel(
|
||||
val user: User,
|
||||
) : FeedViewModel(UserProfileReportsFeedFilter(user)) {
|
||||
) : AndroidFeedViewModel(UserProfileReportsFeedFilter(user)) {
|
||||
class Factory(
|
||||
val user: User,
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
+1
-1
@@ -29,11 +29,11 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
|
||||
+1
-1
@@ -25,11 +25,11 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
+2
-32
@@ -48,7 +48,6 @@ import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -56,8 +55,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
@@ -74,6 +71,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus
|
||||
@@ -83,7 +82,6 @@ import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
@@ -360,34 +358,6 @@ fun RenderThreadFeed(
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a Zebra pattern where each bar is a reply level.
|
||||
fun Modifier.drawReplyLevel(
|
||||
level: State<Int>,
|
||||
color: Color,
|
||||
selected: Color,
|
||||
): Modifier =
|
||||
this
|
||||
.drawBehind {
|
||||
val paddingDp = 2
|
||||
val strokeWidthDp = 2
|
||||
val levelWidthDp = strokeWidthDp + 1
|
||||
|
||||
val padding = paddingDp.dp.toPx()
|
||||
val strokeWidth = strokeWidthDp.dp.toPx()
|
||||
val levelWidth = levelWidthDp.dp.toPx()
|
||||
|
||||
repeat(level.value) {
|
||||
this.drawLine(
|
||||
if (it == level.value - 1) selected else color,
|
||||
Offset(padding + it * levelWidth, 0f),
|
||||
Offset(padding + it * levelWidth, size.height),
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
}
|
||||
|
||||
return@drawBehind
|
||||
}.padding(start = (2 + (level.value * 3)).dp)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NoteMaster(
|
||||
|
||||
+10
-69
@@ -20,76 +20,17 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal
|
||||
|
||||
import androidx.compose.foundation.interaction.DragInteraction
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ThreadLevelCalculator
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
|
||||
abstract class LevelFeedViewModel(
|
||||
// Re-export from commons for backwards compatibility
|
||||
typealias LevelFeedViewModel = com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel
|
||||
|
||||
/**
|
||||
* Android-specific LevelFeedViewModel base class that provides LocalCache as the cache provider.
|
||||
* Subclasses can extend this to automatically use LocalCache without passing cacheProvider.
|
||||
*/
|
||||
abstract class AndroidLevelFeedViewModel(
|
||||
localFilter: FeedFilter<Note>,
|
||||
) : FeedViewModel(localFilter) {
|
||||
var llState: LazyListState by mutableStateOf(LazyListState(0, 0))
|
||||
|
||||
val hasDragged = mutableStateOf(false)
|
||||
|
||||
val selectedIDHex =
|
||||
llState.interactionSource.interactions
|
||||
.onEach {
|
||||
if (it is DragInteraction.Start) {
|
||||
hasDragged.value = true
|
||||
}
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.Eagerly,
|
||||
null,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val levelCacheFlow: StateFlow<Map<Note, Int>> =
|
||||
feedState.feedContent
|
||||
.transformLatest { feed ->
|
||||
emitAll(
|
||||
if (feed is FeedState.Loaded) {
|
||||
feed.feed.map {
|
||||
val cache = mutableMapOf<Note, Int>()
|
||||
it.list.forEach {
|
||||
ThreadLevelCalculator.replyLevel(it, cache)
|
||||
}
|
||||
cache
|
||||
}
|
||||
} else {
|
||||
MutableStateFlow(mapOf())
|
||||
},
|
||||
)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5000),
|
||||
mapOf(),
|
||||
)
|
||||
|
||||
fun levelFlowForItem(note: Note) =
|
||||
levelCacheFlow
|
||||
.map {
|
||||
it[note] ?: 0
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
) : LevelFeedViewModel(localFilter, LocalCache)
|
||||
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LevelSignature
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
import com.vitorpamplona.amethyst.model.ThreadLevelCalculator
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@Immutable
|
||||
class ThreadFeedFilter(
|
||||
val account: Account,
|
||||
private val noteId: String,
|
||||
) : FeedFilter<Note>() {
|
||||
override fun feedKey(): String = noteId
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val cachedSignatures: MutableMap<Note, LevelSignature> = mutableMapOf()
|
||||
val followingKeySet = account.kind3FollowList.flow.value.authors
|
||||
val eventsToWatch = ThreadAssembler().findThreadFor(noteId) ?: return emptyList()
|
||||
|
||||
// Filter out drafts made by other accounts on device
|
||||
val filteredEvents =
|
||||
eventsToWatch.allNotes
|
||||
.filter { !it.isDraft() || (it.author?.pubkeyHex == account.userProfile().pubkeyHex) }
|
||||
.toImmutableSet()
|
||||
val filteredThreadInfo = ThreadAssembler.ThreadInfo(eventsToWatch.root, filteredEvents)
|
||||
|
||||
val eventsInHex = filteredThreadInfo.allNotes.map { it.idHex }.toSet()
|
||||
val now = TimeUtils.now()
|
||||
|
||||
// Currently orders by date of each event, descending, at each level of the reply stack
|
||||
val order =
|
||||
compareByDescending<Note> {
|
||||
ThreadLevelCalculator
|
||||
.replyLevelSignature(
|
||||
it,
|
||||
eventsInHex,
|
||||
cachedSignatures,
|
||||
account.userProfile(),
|
||||
followingKeySet,
|
||||
now,
|
||||
).signature
|
||||
}
|
||||
|
||||
return filteredThreadInfo.allNotes.sortedWith(order)
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -22,12 +22,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
|
||||
class ThreadFeedViewModel(
|
||||
account: Account,
|
||||
noteId: String,
|
||||
) : LevelFeedViewModel(ThreadFeedFilter(account, noteId)) {
|
||||
) : com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel(ThreadFeedFilter(account, noteId, LocalCache), LocalCache) {
|
||||
class Factory(
|
||||
val account: Account,
|
||||
val noteId: String,
|
||||
|
||||
+5
-4
@@ -20,8 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.ThreadAssembler
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindAddress
|
||||
@@ -53,9 +53,10 @@ fun filterMissingEventsForThread(
|
||||
|
||||
val missingAddresses =
|
||||
mapOfSet {
|
||||
if (threadInfo.root.event == null && threadInfo.root is AddressableNote) {
|
||||
potentialRelaysToFindEvent(threadInfo.root).ifEmpty { defaultRelays }.forEach { relayUrl ->
|
||||
add(relayUrl, threadInfo.root.address)
|
||||
val rootNote = threadInfo.root
|
||||
if (rootNote.event == null && rootNote is AddressableNote) {
|
||||
potentialRelaysToFindEvent(rootNote).ifEmpty { defaultRelays }.forEach { relayUrl ->
|
||||
add(relayUrl, rootNote.address)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -20,7 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies
|
||||
|
||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
import com.vitorpamplona.amethyst.commons.model.ThreadAssembler
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState
|
||||
@@ -45,7 +46,7 @@ class ThreadEventLoaderSubAssembler(
|
||||
key: ThreadQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter>? {
|
||||
val branches = ThreadAssembler().findThreadFor(key.eventId) ?: return null
|
||||
val branches = ThreadAssembler(LocalCache).findThreadFor(key.eventId) ?: return null
|
||||
val defaultRelays = key.account.followPlusAllMineWithSearch.flow.value
|
||||
return filterMissingEventsForThread(branches, defaultRelays)
|
||||
}
|
||||
|
||||
+3
-2
@@ -20,7 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies
|
||||
|
||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
import com.vitorpamplona.amethyst.commons.model.ThreadAssembler
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState
|
||||
@@ -42,7 +43,7 @@ class ThreadFilterSubAssembler(
|
||||
key: ThreadQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter>? {
|
||||
val root = ThreadAssembler().findRoot(key.eventId) ?: return null
|
||||
val root = ThreadAssembler(LocalCache).findRoot(key.eventId) ?: return null
|
||||
|
||||
return filterEventsInThreadForRoot(root, since)
|
||||
}
|
||||
|
||||
+2
-2
@@ -51,14 +51,14 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||
|
||||
Reference in New Issue
Block a user