Finish the migration from LiveData to Flow

Moves most of the flow updates away from the Main thread.
Removes the need to use the Main thread to access Account.userProfile
This commit is contained in:
Vitor Pamplona
2025-04-24 14:35:22 -04:00
parent e375b09a24
commit 067f353e50
104 changed files with 1166 additions and 1079 deletions
-4
View File
@@ -201,9 +201,6 @@ dependencies {
// Navigation // Navigation
implementation libs.androidx.navigation.compose implementation libs.androidx.navigation.compose
// Observe Live data as State
implementation libs.androidx.runtime.livedata
// Material 3 Design // Material 3 Design
implementation libs.androidx.material3 implementation libs.androidx.material3
implementation libs.androidx.material.icons implementation libs.androidx.material.icons
@@ -216,7 +213,6 @@ dependencies {
implementation libs.androidx.lifecycle.runtime.ktx implementation libs.androidx.lifecycle.runtime.ktx
implementation libs.androidx.lifecycle.runtime.compose implementation libs.androidx.lifecycle.runtime.compose
implementation libs.androidx.lifecycle.viewmodel.compose implementation libs.androidx.lifecycle.viewmodel.compose
implementation libs.androidx.lifecycle.livedata.ktx
// Zoomable images // Zoomable images
implementation libs.zoomable implementation libs.zoomable
@@ -72,8 +72,6 @@ fun debugState(context: Context) {
Log.d( Log.d(
"STATE DUMP", "STATE DUMP",
"Notes: " + "Notes: " +
LocalCache.notes.filter { _, it -> it.liveSet != null }.size +
" / " +
LocalCache.notes.filter { _, it -> it.flowSet != null }.size + LocalCache.notes.filter { _, it -> it.flowSet != null }.size +
" / " + " / " +
LocalCache.notes.filter { _, it -> it.event != null }.size + LocalCache.notes.filter { _, it -> it.event != null }.size +
@@ -83,8 +81,6 @@ fun debugState(context: Context) {
Log.d( Log.d(
"STATE DUMP", "STATE DUMP",
"Addressables: " + "Addressables: " +
LocalCache.addressables.filter { _, it -> it.liveSet != null }.size +
" / " +
LocalCache.addressables.filter { _, it -> it.flowSet != null }.size + LocalCache.addressables.filter { _, it -> it.flowSet != null }.size +
" / " + " / " +
LocalCache.addressables.filter { _, it -> it.event != null }.size + LocalCache.addressables.filter { _, it -> it.event != null }.size +
@@ -94,8 +90,6 @@ fun debugState(context: Context) {
Log.d( Log.d(
"STATE DUMP", "STATE DUMP",
"Users: " + "Users: " +
LocalCache.users.filter { _, it -> it.liveSet != null }.size +
" / " +
LocalCache.users.filter { _, it -> it.flowSet != null }.size + LocalCache.users.filter { _, it -> it.flowSet != null }.size +
" / " + " / " +
LocalCache.users.filter { _, it -> it.latestMetadata != null }.size + LocalCache.users.filter { _, it -> it.latestMetadata != null }.size +
@@ -169,3 +163,21 @@ inline fun <T> logTime(
} else { } else {
block() block()
} }
inline fun debug(
tag: String,
debugMessage: String,
) {
if (isDebug) {
Log.d(tag, debugMessage)
}
}
inline fun debug(
tag: String,
debugMessage: () -> String,
) {
if (isDebug) {
Log.d(tag, debugMessage())
}
}
@@ -23,10 +23,6 @@ package com.vitorpamplona.amethyst.model
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.fonfon.kgeohash.GeoHash import com.fonfon.kgeohash.GeoHash
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
@@ -192,6 +188,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -584,7 +581,6 @@ class Account(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val liveKind3FollowsFlow: Flow<LiveFollowList> = val liveKind3FollowsFlow: Flow<LiveFollowList> =
userProfile().flow().follows.stateFlow.transformLatest { userProfile().flow().follows.stateFlow.transformLatest {
checkNotInMainThread()
emit(buildFollowLists(it.user.latestContactList)) emit(buildFollowLists(it.user.latestContactList))
} }
@@ -1090,24 +1086,25 @@ class Account(
) )
} }
val liveHiddenUsers = flowHiddenUsers.asLiveData() val decryptBookmarks: Flow<BookmarkListEvent?> by lazy {
userProfile()
val decryptBookmarks: LiveData<BookmarkListEvent?> by lazy { .flow()
userProfile().live().bookmarks.switchMap { userState -> .bookmarks.stateFlow
liveData(Dispatchers.IO) { .map { userState ->
if (userState.user.latestBookmarkList == null) { if (userState.user.latestBookmarkList == null) {
emit(null) null
} else { } else {
emit( val result =
tryAndWait { continuation -> tryAndWait { continuation ->
userState.user.latestBookmarkList?.privateTags(signer) { userState.user.latestBookmarkList?.privateTags(signer) {
continuation.resume(userState.user.latestBookmarkList) continuation.resume(userState.user.latestBookmarkList)
} }
},
)
}
} }
result
} }
}.debounce(1000)
.flowOn(Dispatchers.Default)
} }
class EmojiMedia( class EmojiMedia(
@@ -21,9 +21,7 @@
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.commons.data.LargeCache import com.vitorpamplona.amethyst.commons.data.LargeCache
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.ammolite.relays.BundledUpdate
@@ -43,6 +41,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Hex
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@Stable @Stable
class PublicChatChannel( class PublicChatChannel(
@@ -93,7 +92,7 @@ class PublicChatChannel(
return info.picture ?: super.profilePicture() return info.picture ?: super.profilePicture()
} }
override fun anyNameStartsWith(prefix: String): Boolean = listOfNotNull(info.name, info.about).filter { it.contains(prefix, true) }.isNotEmpty() override fun anyNameStartsWith(prefix: String): Boolean = listOfNotNull(info.name, info.about).any { it.contains(prefix, true) }
} }
@Stable @Stable
@@ -182,7 +181,7 @@ abstract class Channel(
this.creator = creator this.creator = creator
this.updatedMetadataAt = updatedAt this.updatedMetadataAt = updatedAt
live.invalidateData() flow.invalidateData()
} }
@Synchronized @Synchronized
@@ -227,7 +226,7 @@ abstract class Channel(
abstract fun anyNameStartsWith(prefix: String): Boolean abstract fun anyNameStartsWith(prefix: String): Boolean
// Observers line up here. // Observers line up here.
val live: ChannelLiveData = ChannelLiveData(this) val flow: ChannelFlow = ChannelFlow(this)
fun pruneOldMessages(): Set<Note> { fun pruneOldMessages(): Set<Note> {
val important = val important =
@@ -257,22 +256,24 @@ abstract class Channel(
} }
} }
class ChannelLiveData( class ChannelFlow(
val channel: Channel, val channel: Channel,
) : LiveData<ChannelState>(ChannelState(channel)) { ) {
// Refreshes observers in batches. // Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.IO) private val bundler = BundledUpdate(300, Dispatchers.IO)
val stateFlow = MutableStateFlow(ChannelState(channel))
fun invalidateData() { fun invalidateData() {
checkNotInMainThread()
bundler.invalidate { bundler.invalidate {
checkNotInMainThread() stateFlow.emit(ChannelState(channel))
if (hasActiveObservers()) {
postValue(ChannelState(channel))
} }
} }
fun destroy() {
bundler.cancel()
} }
fun hasObservers() = stateFlow.subscriptionCount.value > 0
} }
class ChannelState( class ChannelState(
@@ -87,7 +87,7 @@ class Chatroom {
} else { } else {
// Old messages, keep the last one. // Old messages, keep the last one.
sorted.take(1).toSet() sorted.take(1).toSet()
} + sorted.filter { it.liveSet?.isInUse() ?: false } + sorted.filter { it.event !is PrivateDmEvent } } + sorted.filter { it.flowSet?.isInUse() ?: false } + sorted.filter { it.event !is PrivateDmEvent }
val toRemove = roomMessages.minus(toKeep) val toRemove = roomMessages.minus(toKeep)
roomMessages = toKeep roomMessages = toKeep
@@ -938,7 +938,7 @@ object LocalCache : ILocalCache {
if (event.createdAt > (note.createdAt() ?: 0)) { if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList()) note.loadEvent(event, author, emptyList())
author.liveSet?.statuses?.invalidateData() author.flowSet?.statuses?.invalidateData()
refreshObservers(note) refreshObservers(note)
} }
@@ -963,7 +963,7 @@ object LocalCache : ILocalCache {
if (version.event == null) { if (version.event == null) {
version.loadEvent(event, author, emptyList()) version.loadEvent(event, author, emptyList())
version.liveSet?.ots?.invalidateData() version.flowSet?.ots?.invalidateData()
} }
refreshObservers(version) refreshObservers(version)
@@ -1245,7 +1245,6 @@ object LocalCache : ILocalCache {
} }
deleteNote.clearFlow() deleteNote.clearFlow()
deleteNote.clearLive()
notes.remove(deleteNote.idHex) notes.remove(deleteNote.idHex)
} }
@@ -1259,7 +1258,6 @@ object LocalCache : ILocalCache {
deleteWraps(noteEvent) deleteWraps(noteEvent)
} }
it.clearFlow() it.clearFlow()
it.clearLive()
} }
notes.remove(it.id) notes.remove(it.id)
@@ -1379,7 +1377,7 @@ object LocalCache : ILocalCache {
mentions.forEach { mentions.forEach {
// doesn't add to reports, but triggers recounts // doesn't add to reports, but triggers recounts
it.liveSet?.reports?.invalidateData() it.flowSet?.reports?.invalidateData()
} }
} }
@@ -1625,7 +1623,7 @@ object LocalCache : ILocalCache {
checkGetOrCreateNote(it.eventId)?.let { editedNote -> checkGetOrCreateNote(it.eventId)?.let { editedNote ->
modificationCache.remove(editedNote.idHex) modificationCache.remove(editedNote.idHex)
// must update list of Notes to quickly update the user. // must update list of Notes to quickly update the user.
editedNote.liveSet?.edits?.invalidateData() editedNote.flowSet?.edits?.invalidateData()
} }
} }
@@ -2056,10 +2054,6 @@ object LocalCache : ILocalCache {
} }
fun cleanObservers() { fun cleanObservers() {
notes.forEach { _, it -> it.clearLive() }
addressables.forEach { _, it -> it.clearLive() }
users.forEach { _, it -> it.clearLive() }
notes.forEach { _, it -> it.clearFlow() } notes.forEach { _, it -> it.clearFlow() }
addressables.forEach { _, it -> it.clearFlow() } addressables.forEach { _, it -> it.clearFlow() }
users.forEach { _, it -> it.clearFlow() } users.forEach { _, it -> it.clearFlow() }
@@ -2213,8 +2207,8 @@ object LocalCache : ILocalCache {
note.event is ReportEvent || note.event is ReportEvent ||
note.event is GenericRepostEvent note.event is GenericRepostEvent
) && ) &&
note.replyTo?.any { it.liveSet?.isInUse() == true } != true && note.replyTo?.any { it.flowSet?.isInUse() == true } != true &&
note.liveSet?.isInUse() != true && note.flowSet?.isInUse() != true &&
// don't delete if observing. // don't delete if observing.
note.author?.pubkeyHex !in note.author?.pubkeyHex !in
accounts && accounts &&
@@ -2278,7 +2272,6 @@ object LocalCache : ILocalCache {
} }
note.clearFlow() note.clearFlow()
note.clearLive()
notes.remove(note.idHex) notes.remove(note.idHex)
} }
@@ -2313,12 +2306,10 @@ object LocalCache : ILocalCache {
val childrenToBeRemoved = mutableListOf<Note>() val childrenToBeRemoved = mutableListOf<Note>()
val toBeRemoved = val toBeRemoved =
account.liveHiddenUsers.value account.flowHiddenUsers.value.hiddenUsers
?.hiddenUsers .map { userHex ->
?.map { userHex ->
(notes.filter { _, it -> it.event?.pubKey == userHex } + addressables.filter { _, it -> it.event?.pubKey == userHex }).toSet() (notes.filter { _, it -> it.event?.pubKey == userHex } + addressables.filter { _, it -> it.event?.pubKey == userHex }).toSet()
}?.flatten() }.flatten()
?: emptyList()
toBeRemoved.forEach { toBeRemoved.forEach {
removeFromCache(it) removeFromCache(it)
@@ -2337,7 +2328,7 @@ object LocalCache : ILocalCache {
users.forEach { _, user -> users.forEach { _, user ->
if ( if (
user.pubkeyHex !in loggedIn && user.pubkeyHex !in loggedIn &&
(user.liveSet == null || user.liveSet?.isInUse() == false) && (user.flowSet == null || user.flowSet?.isInUse() == false) &&
user.latestContactList != null user.latestContactList != null
) { ) {
user.latestContactList = null user.latestContactList = null
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.launchAndWaitAll import com.vitorpamplona.amethyst.launchAndWaitAll
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
@@ -228,29 +227,36 @@ open class Note(
this.author = author this.author = author
this.replyTo = replyTo this.replyTo = replyTo
liveSet?.metadata?.invalidateData()
flowSet?.metadata?.invalidateData() flowSet?.metadata?.invalidateData()
} }
} }
fun hasZapsBoostsOrReactions(): Boolean = reactions.isNotEmpty() || zaps.isNotEmpty() || boosts.isNotEmpty()
fun countReactions(): Int {
var total = 0
reactions.forEach { total += it.value.size }
return total
}
fun addReply(note: Note) { fun addReply(note: Note) {
if (note !in replies) { if (note !in replies) {
replies = replies + note replies = replies + note
liveSet?.replies?.invalidateData() flowSet?.replies?.invalidateData()
} }
} }
fun removeReply(note: Note) { fun removeReply(note: Note) {
if (note in replies) { if (note in replies) {
replies = replies - note replies = replies - note
liveSet?.replies?.invalidateData() flowSet?.replies?.invalidateData()
} }
} }
fun removeBoost(note: Note) { fun removeBoost(note: Note) {
if (note in boosts) { if (note in boosts) {
boosts = boosts - note boosts = boosts - note
liveSet?.boosts?.invalidateData() flowSet?.boosts?.invalidateData()
} }
} }
@@ -281,13 +287,11 @@ open class Note(
relays = listOf<RelayBriefInfoCache.RelayBriefInfo>() relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
lastReactionsDownloadTime = emptyMap() lastReactionsDownloadTime = emptyMap()
if (repliesChanged) liveSet?.replies?.invalidateData() if (repliesChanged) flowSet?.replies?.invalidateData()
if (reactionsChanged) liveSet?.reactions?.invalidateData() if (reactionsChanged) flowSet?.reactions?.invalidateData()
if (boostsChanged) liveSet?.boosts?.invalidateData() if (boostsChanged) flowSet?.boosts?.invalidateData()
if (reportsChanged) { if (reportsChanged) flowSet?.reports?.invalidateData()
flowSet?.reports?.invalidateData() if (zapsChanged) flowSet?.zaps?.invalidateData()
}
if (zapsChanged) liveSet?.zaps?.invalidateData()
return toBeRemoved return toBeRemoved
} }
@@ -306,7 +310,7 @@ open class Note(
reactions = reactions + Pair(reaction, newList) reactions = reactions + Pair(reaction, newList)
} }
liveSet?.reactions?.invalidateData() flowSet?.reactions?.invalidateData()
} }
} }
} }
@@ -327,28 +331,28 @@ open class Note(
if (zaps[note] != null) { if (zaps[note] != null) {
zaps = zaps.minus(note) zaps = zaps.minus(note)
updateZapTotal() updateZapTotal()
liveSet?.zaps?.invalidateData() flowSet?.zaps?.invalidateData()
} else if (zaps.containsValue(note)) { } else if (zaps.containsValue(note)) {
zaps = zaps.filterValues { it != note } zaps = zaps.filterValues { it != note }
updateZapTotal() updateZapTotal()
liveSet?.zaps?.invalidateData() flowSet?.zaps?.invalidateData()
} }
} }
fun removeZapPayment(note: Note) { fun removeZapPayment(note: Note) {
if (zapPayments.containsKey(note)) { if (zapPayments.containsKey(note)) {
zapPayments = zapPayments.minus(note) zapPayments = zapPayments.minus(note)
liveSet?.zaps?.invalidateData() flowSet?.zaps?.invalidateData()
} else if (zapPayments.containsValue(note)) { } else if (zapPayments.containsValue(note)) {
zapPayments = zapPayments.filterValues { it != note } zapPayments = zapPayments.filterValues { it != note }
liveSet?.zaps?.invalidateData() flowSet?.zaps?.invalidateData()
} }
} }
fun addBoost(note: Note) { fun addBoost(note: Note) {
if (note !in boosts) { if (note !in boosts) {
boosts = boosts + note boosts = boosts + note
liveSet?.boosts?.invalidateData() flowSet?.boosts?.invalidateData()
} }
} }
@@ -375,7 +379,7 @@ open class Note(
val inserted = innerAddZap(zapRequest, zap) val inserted = innerAddZap(zapRequest, zap)
if (inserted) { if (inserted) {
updateZapTotal() updateZapTotal()
liveSet?.zaps?.invalidateData() flowSet?.zaps?.invalidateData()
} }
} }
} }
@@ -401,7 +405,7 @@ open class Note(
if (zapPayments[zapPaymentRequest] == null) { if (zapPayments[zapPaymentRequest] == null) {
val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment) val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment)
if (inserted) { if (inserted) {
liveSet?.zaps?.invalidateData() flowSet?.zaps?.invalidateData()
} }
} }
} }
@@ -413,10 +417,10 @@ open class Note(
val listOfAuthors = reactions[reaction] val listOfAuthors = reactions[reaction]
if (listOfAuthors == null) { if (listOfAuthors == null) {
reactions = reactions + Pair(reaction, listOf(note)) reactions = reactions + Pair(reaction, listOf(note))
liveSet?.reactions?.invalidateData() flowSet?.reactions?.invalidateData()
} else if (!listOfAuthors.contains(note)) { } else if (!listOfAuthors.contains(note)) {
reactions = reactions + Pair(reaction, listOfAuthors + note) reactions = reactions + Pair(reaction, listOfAuthors + note)
liveSet?.reactions?.invalidateData() flowSet?.reactions?.invalidateData()
} }
} }
@@ -866,36 +870,8 @@ open class Note(
return false return false
} }
var liveSet: NoteLiveSet? = null
var flowSet: NoteFlowSet? = null var flowSet: NoteFlowSet? = null
@Synchronized
fun createOrDestroyLiveSync(create: Boolean) {
if (create) {
if (liveSet == null) {
liveSet = NoteLiveSet(this)
}
} else {
if (liveSet != null && liveSet?.isInUse() == false) {
liveSet?.destroy()
liveSet = null
}
}
}
fun live(): NoteLiveSet {
if (liveSet == null) {
createOrDestroyLiveSync(true)
}
return liveSet!!
}
fun clearLive() {
if (liveSet != null && liveSet?.isInUse() == false) {
createOrDestroyLiveSync(false)
}
}
@Synchronized @Synchronized
fun createOrDestroyFlowSync(create: Boolean) { fun createOrDestroyFlowSync(create: Boolean) {
if (create) { if (create) {
@@ -970,6 +946,12 @@ class NoteFlowSet(
val metadata = NoteBundledRefresherFlow(u) val metadata = NoteBundledRefresherFlow(u)
val reports = NoteBundledRefresherFlow(u) val reports = NoteBundledRefresherFlow(u)
val relays = NoteBundledRefresherFlow(u) val relays = NoteBundledRefresherFlow(u)
val reactions = NoteBundledRefresherFlow(u)
val boosts = NoteBundledRefresherFlow(u)
val replies = NoteBundledRefresherFlow(u)
val zaps = NoteBundledRefresherFlow(u)
val ots = NoteBundledRefresherFlow(u)
val edits = NoteBundledRefresherFlow(u)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun author() = fun author() =
@@ -981,31 +963,9 @@ class NoteFlowSet(
} }
fun isInUse(): Boolean = fun isInUse(): Boolean =
metadata.stateFlow.subscriptionCount.value > 0 || metadata.hasObservers() ||
reports.stateFlow.subscriptionCount.value > 0 || reports.hasObservers() ||
relays.stateFlow.subscriptionCount.value > 0 relays.hasObservers() ||
fun destroy() {
metadata.destroy()
reports.destroy()
relays.destroy()
}
}
@Stable
class NoteLiveSet(
u: Note,
) {
// Observers line up here.
val metadata = NoteBundledRefresherLiveData(u)
val reactions = NoteBundledRefresherLiveData(u)
val boosts = NoteBundledRefresherLiveData(u)
val replies = NoteBundledRefresherLiveData(u)
val zaps = NoteBundledRefresherLiveData(u)
val ots = NoteBundledRefresherLiveData(u)
val edits = NoteBundledRefresherLiveData(u)
fun isInUse(): Boolean =
metadata.hasObservers() || metadata.hasObservers() ||
reactions.hasObservers() || reactions.hasObservers() ||
boosts.hasObservers() || boosts.hasObservers() ||
@@ -1016,6 +976,8 @@ class NoteLiveSet(
fun destroy() { fun destroy() {
metadata.destroy() metadata.destroy()
reports.destroy()
relays.destroy()
reactions.destroy() reactions.destroy()
boosts.destroy() boosts.destroy()
replies.destroy() replies.destroy()
@@ -1039,38 +1001,15 @@ class NoteBundledRefresherFlow(
} }
fun invalidateData() { fun invalidateData() {
checkNotInMainThread()
bundler.invalidate { bundler.invalidate {
checkNotInMainThread()
stateFlow.emit(NoteState(note)) stateFlow.emit(NoteState(note))
} }
} }
fun hasObservers() = stateFlow.subscriptionCount.value > 0
} }
@Stable @Immutable
class NoteBundledRefresherLiveData( class NoteState(
val note: Note,
) : LiveData<NoteState>(NoteState(note)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
fun destroy() {
bundler.cancel()
}
fun invalidateData() {
checkNotInMainThread()
bundler.invalidate {
checkNotInMainThread()
postValue(NoteState(note))
}
}
}
@Immutable class NoteState(
val note: Note, val note: Note,
) )
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.ammolite.relays.BundledUpdate
@@ -125,7 +124,7 @@ class User(
if (event.id == latestBookmarkList?.id) return if (event.id == latestBookmarkList?.id) return
latestBookmarkList = event latestBookmarkList = event
liveSet?.bookmarks?.invalidateData() flowSet?.bookmarks?.invalidateData()
} }
fun clearEOSE() { fun clearEOSE() {
@@ -139,7 +138,6 @@ class User(
latestContactList = event latestContactList = event
// Update following of the current user // Update following of the current user
liveSet?.follows?.invalidateData()
flowSet?.follows?.invalidateData() flowSet?.follows?.invalidateData()
// Update Followers of the past user list // Update Followers of the past user list
@@ -147,19 +145,18 @@ class User(
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
LocalCache LocalCache
.getUserIfExists(it) .getUserIfExists(it)
?.liveSet ?.flowSet
?.followers ?.followers
?.invalidateData() ?.invalidateData()
} }
(latestContactList)?.unverifiedFollowKeySet()?.forEach { (latestContactList)?.unverifiedFollowKeySet()?.forEach {
LocalCache LocalCache
.getUserIfExists(it) .getUserIfExists(it)
?.liveSet ?.flowSet
?.followers ?.followers
?.invalidateData() ?.invalidateData()
} }
liveSet?.relays?.invalidateData()
flowSet?.relays?.invalidateData() flowSet?.relays?.invalidateData()
} }
@@ -169,10 +166,10 @@ class User(
val reportsBy = reports[author] val reportsBy = reports[author]
if (reportsBy == null) { if (reportsBy == null) {
reports = reports + Pair(author, setOf(note)) reports = reports + Pair(author, setOf(note))
liveSet?.reports?.invalidateData() flowSet?.reports?.invalidateData()
} else if (!reportsBy.contains(note)) { } else if (!reportsBy.contains(note)) {
reports = reports + Pair(author, reportsBy + note) reports = reports + Pair(author, reportsBy + note)
liveSet?.reports?.invalidateData() flowSet?.reports?.invalidateData()
} }
} }
@@ -182,7 +179,7 @@ class User(
if (reports[author]?.contains(deleteNote) == true) { if (reports[author]?.contains(deleteNote) == true) {
reports[author]?.let { reports[author]?.let {
reports = reports + Pair(author, it.minus(deleteNote)) reports = reports + Pair(author, it.minus(deleteNote))
liveSet?.reports?.invalidateData() flowSet?.reports?.invalidateData()
} }
} }
} }
@@ -193,17 +190,17 @@ class User(
) { ) {
if (zaps[zapRequest] == null) { if (zaps[zapRequest] == null) {
zaps = zaps + Pair(zapRequest, zap) zaps = zaps + Pair(zapRequest, zap)
liveSet?.zaps?.invalidateData() flowSet?.zaps?.invalidateData()
} }
} }
fun removeZap(zapRequestOrZapEvent: Note) { fun removeZap(zapRequestOrZapEvent: Note) {
if (zaps.containsKey(zapRequestOrZapEvent)) { if (zaps.containsKey(zapRequestOrZapEvent)) {
zaps = zaps.minus(zapRequestOrZapEvent) zaps = zaps.minus(zapRequestOrZapEvent)
liveSet?.zaps?.invalidateData() flowSet?.zaps?.invalidateData()
} else if (zaps.containsValue(zapRequestOrZapEvent)) { } else if (zaps.containsValue(zapRequestOrZapEvent)) {
zaps = zaps.filter { it.value != zapRequestOrZapEvent } zaps = zaps.filter { it.value != zapRequestOrZapEvent }
liveSet?.zaps?.invalidateData() flowSet?.zaps?.invalidateData()
} }
} }
@@ -256,7 +253,7 @@ class User(
val privateChatroom = getOrCreatePrivateChatroom(room) val privateChatroom = getOrCreatePrivateChatroom(room)
if (msg !in privateChatroom.roomMessages) { if (msg !in privateChatroom.roomMessages) {
privateChatroom.addMessageSync(msg) privateChatroom.addMessageSync(msg)
liveSet?.messages?.invalidateData() flowSet?.messages?.invalidateData()
} }
} }
@@ -267,7 +264,7 @@ class User(
val privateChatroom = getOrCreatePrivateChatroom(user) val privateChatroom = getOrCreatePrivateChatroom(user)
if (msg !in privateChatroom.roomMessages) { if (msg !in privateChatroom.roomMessages) {
privateChatroom.addMessageSync(msg) privateChatroom.addMessageSync(msg)
liveSet?.messages?.invalidateData() flowSet?.messages?.invalidateData()
} }
} }
@@ -284,7 +281,7 @@ class User(
val privateChatroom = getOrCreatePrivateChatroom(user) val privateChatroom = getOrCreatePrivateChatroom(user)
if (msg in privateChatroom.roomMessages) { if (msg in privateChatroom.roomMessages) {
privateChatroom.removeMessageSync(msg) privateChatroom.removeMessageSync(msg)
liveSet?.messages?.invalidateData() flowSet?.messages?.invalidateData()
} }
} }
@@ -296,7 +293,7 @@ class User(
val privateChatroom = getOrCreatePrivateChatroom(room) val privateChatroom = getOrCreatePrivateChatroom(room)
if (msg in privateChatroom.roomMessages) { if (msg in privateChatroom.roomMessages) {
privateChatroom.removeMessageSync(msg) privateChatroom.removeMessageSync(msg)
liveSet?.messages?.invalidateData() flowSet?.messages?.invalidateData()
} }
} }
@@ -314,7 +311,7 @@ class User(
here.counter++ here.counter++
} }
liveSet?.relayInfo?.invalidateData() flowSet?.relayInfo?.invalidateData()
} }
fun updateUserInfo( fun updateUserInfo(
@@ -334,7 +331,6 @@ class User(
} }
flowSet?.metadata?.invalidateData() flowSet?.metadata?.invalidateData()
liveSet?.metadata?.invalidateData()
} }
fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
@@ -398,36 +394,8 @@ class User(
fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
var liveSet: UserLiveSet? = null
var flowSet: UserFlowSet? = null var flowSet: UserFlowSet? = null
fun live(): UserLiveSet {
if (liveSet == null) {
createOrDestroyLiveSync(true)
}
return liveSet!!
}
fun clearLive() {
if (liveSet != null && liveSet?.isInUse() == false) {
createOrDestroyLiveSync(false)
}
}
@Synchronized
fun createOrDestroyLiveSync(create: Boolean) {
if (create) {
if (liveSet == null) {
liveSet = UserLiveSet(this)
}
} else {
if (liveSet != null && liveSet?.isInUse() == false) {
liveSet?.destroy()
liveSet = null
}
}
}
@Synchronized @Synchronized
fun createOrDestroyFlowSync(create: Boolean) { fun createOrDestroyFlowSync(create: Boolean) {
if (create) { if (create) {
@@ -464,43 +432,21 @@ class UserFlowSet(
val metadata = UserBundledRefresherFlow(u) val metadata = UserBundledRefresherFlow(u)
val follows = UserBundledRefresherFlow(u) val follows = UserBundledRefresherFlow(u)
val relays = UserBundledRefresherFlow(u) val relays = UserBundledRefresherFlow(u)
val followers = UserBundledRefresherFlow(u)
fun isInUse(): Boolean = val reports = UserBundledRefresherFlow(u)
metadata.stateFlow.subscriptionCount.value > 0 || val messages = UserBundledRefresherFlow(u)
relays.stateFlow.subscriptionCount.value > 0 || val relayInfo = UserBundledRefresherFlow(u)
follows.stateFlow.subscriptionCount.value > 0 val zaps = UserBundledRefresherFlow(u)
val bookmarks = UserBundledRefresherFlow(u)
fun destroy() { val statuses = UserBundledRefresherFlow(u)
metadata.destroy()
relays.destroy()
follows.destroy()
}
}
@Stable
class UserLiveSet(
u: User,
) {
val metadata = UserBundledRefresherLiveData(u)
// UI Observers line up here.
val follows = UserBundledRefresherLiveData(u)
val followers = UserBundledRefresherLiveData(u)
val reports = UserBundledRefresherLiveData(u)
val messages = UserBundledRefresherLiveData(u)
val relays = UserBundledRefresherLiveData(u)
val relayInfo = UserBundledRefresherLiveData(u)
val zaps = UserBundledRefresherLiveData(u)
val bookmarks = UserBundledRefresherLiveData(u)
val statuses = UserBundledRefresherLiveData(u)
fun isInUse(): Boolean = fun isInUse(): Boolean =
metadata.hasObservers() || metadata.hasObservers() ||
relays.hasObservers() ||
follows.hasObservers() || follows.hasObservers() ||
followers.hasObservers() || followers.hasObservers() ||
reports.hasObservers() || reports.hasObservers() ||
messages.hasObservers() || messages.hasObservers() ||
relays.hasObservers() ||
relayInfo.hasObservers() || relayInfo.hasObservers() ||
zaps.hasObservers() || zaps.hasObservers() ||
bookmarks.hasObservers() || bookmarks.hasObservers() ||
@@ -508,11 +454,11 @@ class UserLiveSet(
fun destroy() { fun destroy() {
metadata.destroy() metadata.destroy()
relays.destroy()
follows.destroy() follows.destroy()
followers.destroy() followers.destroy()
reports.destroy() reports.destroy()
messages.destroy() messages.destroy()
relays.destroy()
relayInfo.destroy() relayInfo.destroy()
zaps.destroy() zaps.destroy()
bookmarks.destroy() bookmarks.destroy()
@@ -527,27 +473,6 @@ data class RelayInfo(
var counter: Long, var counter: Long,
) )
class UserBundledRefresherLiveData(
val user: User,
) : LiveData<UserState>(UserState(user)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO)
fun destroy() {
bundler.cancel()
}
fun invalidateData() {
checkNotInMainThread()
bundler.invalidate {
checkNotInMainThread()
postValue(UserState(user))
}
}
}
@Stable @Stable
class UserBundledRefresherFlow( class UserBundledRefresherFlow(
val user: User, val user: User,
@@ -569,8 +494,11 @@ class UserBundledRefresherFlow(
stateFlow.emit(UserState(user)) stateFlow.emit(UserState(user))
} }
} }
fun hasObservers() = stateFlow.subscriptionCount.value > 0
} }
@Immutable class UserState( @Immutable
class UserState(
val user: User, val user: User,
) )
@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import android.content.ContentProviderOperation.newCall
import android.util.Log import android.util.Log
import android.util.LruCache import android.util.LruCache
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.service.location package com.vitorpamplona.amethyst.service.location
import android.content.Context import android.content.Context
import coil3.util.CoilUtils.result
import com.fonfon.kgeohash.GeoHash import com.fonfon.kgeohash.GeoHash
import com.fonfon.kgeohash.toGeoHash import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeohashPrecision import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeohashPrecision
@@ -27,9 +27,7 @@ import android.content.Context.RECEIVER_EXPORTED
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.provider.LiveFolders.INTENT
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat.registerReceiver
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -30,24 +30,20 @@ import com.vitorpamplona.amethyst.service.relayClient.searchCommand.MutableQuery
fun <T> KeyDataSourceSubscription( fun <T> KeyDataSourceSubscription(
state: T, state: T,
dataSource: QueryBasedSubscriptionOrchestrator<T>, dataSource: QueryBasedSubscriptionOrchestrator<T>,
) { ) = DisposableEffect(state) {
DisposableEffect(state) {
dataSource.subscribe(state) dataSource.subscribe(state)
onDispose { onDispose {
dataSource.unsubscribe(state) dataSource.unsubscribe(state)
} }
}
} }
@Composable @Composable
fun <T : MutableQueryState> KeyDataSourceSubscription( fun <T : MutableQueryState> KeyDataSourceSubscription(
state: T, state: T,
dataSource: MutableQueryBasedSubscriptionOrchestrator<T>, dataSource: MutableQueryBasedSubscriptionOrchestrator<T>,
) { ) = DisposableEffect(state) {
DisposableEffect(state) {
dataSource.subscribe(state) dataSource.subscribe(state)
onDispose { onDispose {
dataSource.unsubscribe(state) dataSource.unsubscribe(state)
} }
}
} }
@@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.ammolite.relays.NostrClient
import com.vitorpamplona.ammolite.relays.datasources.RelayAuthenticator import com.vitorpamplona.ammolite.relays.datasources.RelayAuthenticator
import kotlin.collections.forEach
class ScreenAuthAccount( class ScreenAuthAccount(
val account: Account, val account: Account,
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.service.relayClient.authCommand.model package com.vitorpamplona.amethyst.service.relayClient.authCommand.model
import androidx.lifecycle.AtomicReference import java.util.concurrent.atomic.AtomicReference
class ListWithUniqueSetCache<T, U>( class ListWithUniqueSetCache<T, U>(
val key: (T) -> U, val key: (T) -> U,
@@ -20,8 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.service.relayClient.reqCommand package com.vitorpamplona.amethyst.service.relayClient.reqCommand
import android.util.Log
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.ammolite.relays.NostrClient
import com.vitorpamplona.ammolite.relays.datasources.SubscriptionOrchestrator import com.vitorpamplona.ammolite.relays.datasources.SubscriptionOrchestrator
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -48,10 +46,6 @@ abstract class QueryBasedSubscriptionOrchestrator<T>(
} }
invalidateFilters() invalidateFilters()
if (isDebug) {
Log.d(this::class.simpleName, "Watch $query (${queries.size} queries)")
}
} }
// This is called by main. Keep it really fast. // This is called by main. Keep it really fast.
@@ -65,10 +59,6 @@ abstract class QueryBasedSubscriptionOrchestrator<T>(
if (queries.isEmpty()) { if (queries.isEmpty()) {
stop() stop()
} }
if (isDebug) {
Log.d(this::class.simpleName, "Unwatch $query (${queries.size} queries)")
}
} }
fun forEachSubscriber(action: (T) -> Unit) { fun forEachSubscriber(action: (T) -> Unit) {
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2024 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.relayClient.reqCommand.account
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@Composable
fun observeAccountIsHiddenWord(
account: Account,
word: String,
): State<Boolean> {
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(account, word) {
account.flowHiddenUsers
.map { word in it.hiddenWords }
.distinctUntilChanged()
}
return flow.collectAsStateWithLifecycle(false)
}
@Composable
fun observeAccountIsHiddenUser(
account: Account,
user: User,
): State<Boolean> {
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(account, user) {
account.flowHiddenUsers
.map { it.hiddenUsers.contains(user.pubkeyHex) || it.spammers.contains(user.pubkeyHex) }
.distinctUntilChanged()
}
return flow.collectAsStateWithLifecycle(account.isHidden(user))
}
@@ -31,8 +31,6 @@ import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import kotlin.collections.filter
import kotlin.collections.mapNotNull
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class ChannelFinderQueryState( class ChannelFinderQueryState(
@@ -22,13 +22,52 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.ChannelState import com.vitorpamplona.amethyst.model.ChannelState
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
@Composable @Composable
fun observeChannel(baseChannel: Channel): State<ChannelState?> { fun observeChannel(baseChannel: Channel): State<ChannelState?> {
ChannelFinderFilterAssemblerSubscription(baseChannel) ChannelFinderFilterAssemblerSubscription(baseChannel)
return baseChannel.live.observeAsState() return baseChannel.flow.stateFlow.collectAsStateWithLifecycle()
}
@Composable
fun observeChannelPicture(baseChannel: Channel): State<String?> {
// Subscribe in the relay for changes in the metadata of this user.
ChannelFinderFilterAssemblerSubscription(baseChannel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(baseChannel) {
baseChannel
.flow.stateFlow
.mapLatest { it.channel.profilePicture() }
.distinctUntilChanged()
}
return flow.collectAsStateWithLifecycle(baseChannel.profilePicture())
}
@Composable
fun observeChannelInfo(baseChannel: LiveActivitiesChannel): State<LiveActivitiesEvent?> {
// Subscribe in the relay for changes in the metadata of this user.
ChannelFinderFilterAssemblerSubscription(baseChannel)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(baseChannel) {
baseChannel
.flow.stateFlow
.mapLatest { (it.channel as? LiveActivitiesChannel)?.info }
.distinctUntilChanged()
}
return flow.collectAsStateWithLifecycle(baseChannel.info)
} }
@@ -46,7 +46,6 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
import kotlin.collections.mapNotNullTo
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class EventFinderQueryState( class EventFinderQueryState(
@@ -22,22 +22,30 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.note.combineWith
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.sample
@Composable @Composable
fun observeNote(note: Note): State<NoteState?> { fun observeNote(note: Note): State<NoteState> {
// Subscribe in the relay for changes in this note. // Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note.live().metadata.observeAsState() return note
.flow()
.metadata.stateFlow
.collectAsStateWithLifecycle()
} }
@Composable @Composable
@@ -46,11 +54,15 @@ fun <T : Event> observeNoteEvent(note: Note): State<T?> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note val flow =
.live() remember(note) {
.metadata note
.map { it.note.event as? T? } .flow()
.observeAsState(note.event as? T?) .metadata.stateFlow
.mapLatest { it.note.event as? T? }
}
return flow.collectAsStateWithLifecycle(note.event as? T?)
} }
@Composable @Composable
@@ -61,13 +73,18 @@ fun <T> observeNoteAndMap(
// Subscribe in the relay for changes in this note. // Subscribe in the relay for changes in this note.
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device val flow =
return note remember(note) {
.live() note
.metadata .flow()
.map { map(it.note) } .metadata.stateFlow
.mapLatest { map(it.note) }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(map(note)) .flowOn(Dispatchers.Default)
}
// Subscribe in the LocalCache for changes that arrive in the device
return flow.collectAsStateWithLifecycle(map(note))
} }
@Composable @Composable
@@ -79,14 +96,18 @@ fun <T, U> observeNoteEventAndMap(
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note val flow =
.live() remember(note) {
.metadata note
.map { (it.note.event as? T)?.let { map(it) } } .flow()
.metadata.stateFlow
.mapLatest { (it.note.event as? T)?.let { map(it) } }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState( .flowOn(Dispatchers.Default)
(note.event as? T)?.let { map(it) }, }
)
// Subscribe in the LocalCache for changes that arrive in the device
return flow.collectAsStateWithLifecycle((note.event as? T)?.let { map(it) })
} }
@Composable @Composable
@@ -95,12 +116,16 @@ fun observeNoteHasEvent(note: Note): State<Boolean> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note val flow =
.live() remember(note) {
.metadata note
.map { it.note.event != null } .flow()
.metadata.stateFlow
.mapLatest { it.note.event != null }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(note.event != null) }
return flow.collectAsStateWithLifecycle(note.event != null)
} }
@Composable @Composable
@@ -109,7 +134,10 @@ fun observeNoteReplies(note: Note): State<NoteState?> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note.live().replies.observeAsState() return note
.flow()
.replies.stateFlow
.collectAsStateWithLifecycle()
} }
@Composable @Composable
@@ -118,12 +146,17 @@ fun observeNoteReplyCount(note: Note): State<Int> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note val flow =
.live() remember(note) {
.replies note
.map { it.note.reactions.size } .flow()
.reactions.stateFlow
.mapLatest { it.note.replies.size }
.sample(1000)
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(note.reactions.size) }
return flow.collectAsStateWithLifecycle(note.replies.size)
} }
@Composable @Composable
@@ -132,7 +165,10 @@ fun observeNoteReactions(note: Note): State<NoteState?> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note.live().reactions.observeAsState() return note
.flow()
.reactions.stateFlow
.collectAsStateWithLifecycle()
} }
@Composable @Composable
@@ -141,15 +177,19 @@ fun observeNoteReactionCount(note: Note): State<Int> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note val flow =
.live() remember(note) {
.reactions note
.map { .flow()
var total = 0 .reactions.stateFlow
it.note.reactions.forEach { total += it.value.size } .mapLatest { it.note.countReactions() }
total .sample(1000)
}.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(0) .flowOn(Dispatchers.Default)
}
// Subscribe in the LocalCache for changes that arrive in the device
return flow.collectAsStateWithLifecycle(note.countReactions())
} }
@Composable @Composable
@@ -158,7 +198,10 @@ fun observeNoteZaps(note: Note): State<NoteState?> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note.live().zaps.observeAsState() return note
.flow()
.zaps.stateFlow
.collectAsStateWithLifecycle()
} }
@Composable @Composable
@@ -167,7 +210,10 @@ fun observeNoteReposts(note: Note): State<NoteState?> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note.live().boosts.observeAsState() return note
.flow()
.boosts.stateFlow
.collectAsStateWithLifecycle()
} }
@Composable @Composable
@@ -179,12 +225,17 @@ fun observeNoteRepostsBy(
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note val flow =
.live() remember(note) {
.boosts note
.map { it.note.isBoostedBy(user) } .flow()
.boosts.stateFlow
.mapLatest { it.note.isBoostedBy(user) }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(note.isBoostedBy(user)) .flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(note.isBoostedBy(user))
} }
@Composable @Composable
@@ -193,12 +244,17 @@ fun observeNoteRepostCount(note: Note): State<Int> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note val flow =
.live() remember(note) {
.boosts note
.map { it.note.boosts.size } .flow()
.boosts.stateFlow
.sample(1000)
.mapLatest { note.boosts.size }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(note.boosts.size) }
return flow.collectAsStateWithLifecycle(note.boosts.size)
} }
@Composable @Composable
@@ -207,17 +263,18 @@ fun observeNoteReferences(note: Note): State<Boolean> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note val flow =
.live() remember(note) {
.zaps combine(
.combineWith(note.live().boosts, note.live().reactions) { zapState, boostState, reactionState -> note.flow().zaps.stateFlow,
zapState?.note?.zaps?.isNotEmpty() == true || note.flow().boosts.stateFlow,
boostState?.note?.boosts?.isNotEmpty() == true || note.flow().reactions.stateFlow,
reactionState?.note?.reactions?.isNotEmpty() == true ) { zapState, boostState, reactionState ->
zapState.note.hasZapsBoostsOrReactions()
}.distinctUntilChanged() }.distinctUntilChanged()
.observeAsState( }
note.zaps.isNotEmpty() || note.boosts.isNotEmpty() || note.reactions.isNotEmpty(),
) return flow.collectAsStateWithLifecycle(note.hasZapsBoostsOrReactions())
} }
@Composable @Composable
@@ -226,7 +283,11 @@ fun observeNoteOts(note: Note): State<NoteState?> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note.live().ots.observeAsState() return note
.flow()
.ots
.stateFlow
.collectAsStateWithLifecycle()
} }
@Composable @Composable
@@ -235,5 +296,9 @@ fun observeNoteEdits(note: Note): State<NoteState?> {
EventFinderFilterAssemblerSubscription(note) EventFinderFilterAssemblerSubscription(note)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return note.live().edits.observeAsState() return note
.flow()
.edits
.stateFlow
.collectAsStateWithLifecycle()
} }
@@ -22,12 +22,26 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.map import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.sample
import java.math.BigDecimal
@Composable @Composable
fun observeUser(user: User): State<UserState?> { fun observeUser(user: User): State<UserState?> {
@@ -35,7 +49,10 @@ fun observeUser(user: User): State<UserState?> {
UserFinderFilterAssemblerSubscription(user) UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return user.live().metadata.observeAsState() return user
.flow()
.metadata.stateFlow
.collectAsStateWithLifecycle()
} }
@Composable @Composable
@@ -43,13 +60,17 @@ fun observeUserName(user: User): State<String> {
// Subscribe in the relay for changes in the metadata of this user. // Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user) UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device val flow =
return user remember(user) {
.live() user
.metadata .flow()
.map { it.user.toBestDisplayName() } .metadata.stateFlow
.mapLatest { it.user.toBestDisplayName() }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(user.toBestDisplayName()) }
// Subscribe in the LocalCache for changes that arrive in the device
return flow.collectAsStateWithLifecycle(user.toBestDisplayName())
} }
@Composable @Composable
@@ -57,13 +78,17 @@ fun observeUserNip05(user: User): State<String?> {
// Subscribe in the relay for changes in the metadata of this user. // Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user) UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device val flow =
return user remember(user) {
.live() user
.metadata .flow()
.map { it.user.info?.nip05 } .metadata.stateFlow
.mapLatest { it.user.info?.nip05 }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(user.info?.nip05) }
// Subscribe in the LocalCache for changes that arrive in the device
return flow.collectAsStateWithLifecycle(user.info?.nip05)
} }
@Composable @Composable
@@ -72,12 +97,16 @@ fun observeUserAboutMe(user: User): State<String> {
UserFinderFilterAssemblerSubscription(user) UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return user val flow =
.live() remember(user) {
.metadata user
.map { it.user.info?.about ?: "" } .flow()
.metadata.stateFlow
.mapLatest { it.user.info?.about ?: "" }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(user.info?.about ?: "") }
return flow.collectAsStateWithLifecycle(user.info?.about ?: "")
} }
@Composable @Composable
@@ -86,26 +115,34 @@ fun observeUserInfo(user: User): State<UserMetadata?> {
UserFinderFilterAssemblerSubscription(user) UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return user val flow =
.live() remember(user) {
.metadata user
.map { it.user.info } .flow()
.metadata.stateFlow
.mapLatest { it.user.info }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(user.info) }
return flow.collectAsStateWithLifecycle(user.info)
} }
@Composable @Composable
fun observeUserBanner(user: User): State<String> { fun observeUserBanner(user: User): State<String?> {
// Subscribe in the relay for changes in the metadata of this user. // Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user) UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return user val flow =
.live() remember(user) {
.metadata user
.map { it.user.info?.banner ?: "" } .flow()
.metadata.stateFlow
.mapLatest { it.user.info?.banner }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(user.info?.banner ?: "") }
return flow.collectAsStateWithLifecycle(user.info?.banner)
} }
@Composable @Composable
@@ -114,12 +151,16 @@ fun observeUserPicture(user: User): State<String?> {
UserFinderFilterAssemblerSubscription(user) UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return user val flow =
.live() remember(user) {
.metadata user
.map { it.user.info?.picture } .flow()
.metadata.stateFlow
.mapLatest { it.user.info?.picture }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(user.info?.picture) }
return flow.collectAsStateWithLifecycle(user.info?.picture)
} }
@Composable @Composable
@@ -128,10 +169,400 @@ fun observeUserShortName(user: User): State<String> {
UserFinderFilterAssemblerSubscription(user) UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
return user val flow =
.live() remember(user) {
.metadata user
.map { it.user.toBestShortFirstName() } .flow()
.metadata.stateFlow
.mapLatest { it.user.toBestShortFirstName() }
.distinctUntilChanged() .distinctUntilChanged()
.observeAsState(user.toBestShortFirstName()) }
return flow.collectAsStateWithLifecycle(user.toBestShortFirstName())
}
@Composable
fun observeUserFollows(user: User): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
return user
.flow()
.follows.stateFlow
.collectAsStateWithLifecycle()
}
@Composable
fun observeUserFollowCount(user: User): State<Int> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.followers.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.transientFollowCount() ?: 0
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(0)
}
@Composable
fun observeUserTagFollows(user: User): State<Int> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.follows.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.latestContactList?.countFollowTags() ?: 0
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(0)
}
@Composable
fun observeUserBookmarks(user: User): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
return user
.flow()
.bookmarks.stateFlow
.collectAsStateWithLifecycle()
}
@Composable
fun observeUserBookmarkCount(user: User): State<Int> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.followers.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.latestBookmarkList?.countBookmarks() ?: 0
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(0)
}
@Composable
fun observeUserFollowers(user: User): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
return user
.flow()
.followers.stateFlow
.collectAsStateWithLifecycle()
}
@Composable
fun observeUserFollowerCount(user: User): State<Int> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.followers.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.transientFollowerCount()
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(0)
}
@Composable
fun observeUserIsFollowing(
user1: User,
user2: User,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user1)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user1) {
user1
.flow()
.follows.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.isFollowing(user2)
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(user1.isFollowing(user2))
}
@Composable
fun observeUserIsFollowingHashtag(
user: User,
hashtag: String,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.follows.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.isFollowingHashtag(hashtag)
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(user.isFollowingHashtag(hashtag))
}
@Composable
fun observeUserIsFollowingGeohash(
user: User,
geohash: String,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.follows.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.isFollowingGeohash(geohash)
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(user.isFollowingGeohash(geohash))
}
@Composable
fun observeUserIsFollowingChannel(
user: User,
channel: Channel,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.follows.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.latestContactList?.isTaggedEvent(channel.idHex) ?: false
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(user.latestContactList?.isTaggedEvent(channel.idHex) ?: false)
}
@Composable
fun observeUserZaps(user: User): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
return user
.flow()
.zaps.stateFlow
.collectAsStateWithLifecycle()
}
@Composable
fun observeUserZapAmount(user: User): State<BigDecimal> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.zaps.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.zappedAmount()
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(BigDecimal.ZERO)
}
@Composable
fun observeUserReports(user: User): State<UserState?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
return user
.flow()
.reports.stateFlow
.collectAsStateWithLifecycle()
}
@Composable
fun observeUserReportCount(user: User): State<Int> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.reports
.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.reports.values
.sumOf { it.size }
}.distinctUntilChanged()
}
return flow.collectAsStateWithLifecycle(0)
}
@Composable
fun observeUserStatuses(user: User): State<ImmutableList<AddressableNote>> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.statuses
.stateFlow
.sample(1000)
.mapLatest { userState ->
LocalCache.findStatusesForUser(userState.user)
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(persistentListOf())
}
@Composable
fun observeUserRelayIntoList(
user: User,
relayUrl: String,
): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.relayInfo
.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.latestContactList
?.relays()
?.none { it.key == relayUrl } == true
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(false)
}
@Composable
fun observeUserRoomSubject(
user: User,
room: ChatroomKey,
): State<String?> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
user
.flow()
.messages
.stateFlow
.sample(1000)
.mapLatest { userState ->
userState.user.privateChatrooms[room]?.subject
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(user.privateChatrooms[room]?.subject)
}
data class RelayUsage(
val relays: List<String> = emptyList(),
val userRelayList: List<String> = emptyList(),
)
@Composable
fun observeUserRelaysUsing(user: User): State<RelayUsage> {
// Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user)
// Subscribe in the LocalCache for changes that arrive in the device
val flow =
remember(user) {
combine(user.flow().relays.stateFlow, user.flow().relayInfo.stateFlow) { relays, relayInfo ->
val userRelaysBeingUsed = relays.user.relaysBeingUsed.map { it.key }
val currentUserRelays =
relayInfo.user.latestContactList
?.relays()
?.map { RelayUrlFormatter.normalize(it.key) } ?: emptyList()
RelayUsage(userRelaysBeingUsed, currentUserRelays)
}.sample(1000)
.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
return flow.collectAsStateWithLifecycle(RelayUsage())
} }
@@ -20,8 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.service.relayClient.searchCommand package com.vitorpamplona.amethyst.service.relayClient.searchCommand
import android.util.Log
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.ammolite.relays.NostrClient
import com.vitorpamplona.ammolite.relays.datasources.SubscriptionOrchestrator import com.vitorpamplona.ammolite.relays.datasources.SubscriptionOrchestrator
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -64,10 +62,6 @@ abstract class MutableQueryBasedSubscriptionOrchestrator<T : MutableQueryState>(
} }
invalidateFilters() invalidateFilters()
if (isDebug) {
Log.d(this::class.simpleName, "Watch $query (${queries.size} queries)")
}
} }
// This is called by main. Keep it really fast. // This is called by main. Keep it really fast.
@@ -82,10 +76,6 @@ abstract class MutableQueryBasedSubscriptionOrchestrator<T : MutableQueryState>(
if (queries.isEmpty()) { if (queries.isEmpty()) {
stop() stop()
} }
if (isDebug) {
Log.d(this::class.simpleName, "Unwatch $query (${queries.size} queries)")
}
} }
fun forEachSubscriber(action: (T) -> Unit) { fun forEachSubscriber(action: (T) -> Unit) {
@@ -67,7 +67,6 @@ import com.vitorpamplona.quartz.utils.Hex
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.collections.flatten
@Stable @Stable
class SearchQueryState( class SearchQueryState(
@@ -32,6 +32,7 @@ import androidx.annotation.RequiresApi
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk.PICTURES_SUBDIRECTORY
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
@@ -26,7 +26,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import coil3.util.CoilUtils.result
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
@@ -20,8 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import android.R.attr.maxLines
import android.R.attr.onClick
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -50,7 +50,6 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextFieldDefaults.contentPadding
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -29,7 +29,6 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
@@ -30,6 +30,7 @@ import androidx.compose.runtime.remember
import androidx.window.core.layout.WindowHeightSizeClass import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass import androidx.window.core.layout.WindowWidthSizeClass
import com.vitorpamplona.amethyst.ui.components.util.DeviceUtils.screenOrientationIsLocked
object DeviceUtils { object DeviceUtils {
/** /**
@@ -22,22 +22,9 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.Card import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.Card
import com.vitorpamplona.quartz.nip01Core.core.Event
val DefaultFeedOrder: Comparator<Note> = val DefaultFeedOrder: Comparator<Note> =
compareByDescending<Note> compareByDescending<Note> { it.event?.createdAt }.thenBy { it.idHex }
{
val noteEvent = it.event
if (noteEvent == null) {
null
} else {
if (noteEvent is Event) {
noteEvent.createdAt
} else {
null
}
}
}.thenBy { it.idHex }
val DefaultFeedOrderCard: Comparator<Card> = val DefaultFeedOrderCard: Comparator<Card> =
compareByDescending<Card> { it.createdAt() }.thenBy { it.id() } compareByDescending<Card> { it.createdAt() }.thenBy { it.id() }
@@ -58,7 +58,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -91,6 +90,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowerCount
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.ui.actions.mediaServers.MediaServersListView import com.vitorpamplona.amethyst.ui.actions.mediaServers.MediaServersListView
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@@ -381,18 +381,12 @@ private fun FollowingAndFollowerCounts(
baseAccountUser: Account, baseAccountUser: Account,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
val followingCount = baseAccountUser.liveKind3Follows.collectAsStateWithLifecycle()
var followerCount by remember { mutableStateOf("--") }
WatchFollower(baseAccountUser = baseAccountUser) { newFollower ->
if (followerCount != newFollower) {
followerCount = newFollower
}
}
Row( Row(
modifier = drawerSpacing.clickable(onClick = onClick), modifier = drawerSpacing.clickable(onClick = onClick),
) { ) {
val followingCount = baseAccountUser.liveKind3Follows.collectAsStateWithLifecycle()
val followerCount by observeUserFollowerCount(baseAccountUser.userProfile())
Text( Text(
text = text =
followingCount.value.authors.size followingCount.value.authors.size
@@ -405,7 +399,7 @@ private fun FollowingAndFollowerCounts(
Spacer(modifier = DoubleHorzSpacer) Spacer(modifier = DoubleHorzSpacer)
Text( Text(
text = followerCount, text = if (followerCount > 0) followerCount.toString() else "--",
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
@@ -413,22 +407,6 @@ private fun FollowingAndFollowerCounts(
} }
} }
@Composable
fun WatchFollower(
baseAccountUser: Account,
onReady: (String) -> Unit,
) {
val accountUserFollowersState by baseAccountUser
.userProfile()
.live()
.followers
.observeAsState()
LaunchedEffect(key1 = accountUserFollowersState) {
onReady(baseAccountUser.followerCount().toString())
}
}
@Composable @Composable
fun ListContent( fun ListContent(
modifier: Modifier, modifier: Modifier,
@@ -30,7 +30,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.Size23dp import com.vitorpamplona.amethyst.ui.theme.Size23dp
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.String
class BottomBarRoute( class BottomBarRoute(
val route: Route, val route: Route,
@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import android.R.attr.onClick
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -28,7 +27,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -40,8 +40,6 @@ import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.filled.Report
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.AddReaction import androidx.compose.material.icons.outlined.AddReaction
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.PlayCircle import androidx.compose.material.icons.outlined.PlayCircle
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -33,12 +32,11 @@ import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -139,19 +137,9 @@ fun LoadStatuses(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
content: @Composable (ImmutableList<AddressableNote>) -> Unit, content: @Composable (ImmutableList<AddressableNote>) -> Unit,
) { ) {
var statuses: ImmutableList<AddressableNote> by remember { mutableStateOf(persistentListOf()) } val userStatuses by observeUserStatuses(user)
val userStatus by user.live().statuses.observeAsState() content(userStatuses)
LaunchedEffect(key1 = userStatus) {
accountViewModel.findStatusesForUser(userStatus?.user ?: user) { newStatuses ->
if (!equalImmutableLists(statuses, newStatuses)) {
statuses = newStatuses
}
}
}
content(statuses)
} }
@Composable @Composable
@@ -54,11 +54,11 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNip05 import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNip05
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.LoadStatuses
import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon
import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification
import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon
@@ -136,12 +136,11 @@ fun ObserveDisplayNip05Status(
nav: INav, nav: INav,
) { ) {
val nip05 by observeUserNip05(baseUser) val nip05 by observeUserNip05(baseUser)
val statuses by observeUserStatuses(baseUser)
LoadStatuses(baseUser, accountViewModel) { statuses ->
CrossfadeIfEnabled( CrossfadeIfEnabled(
targetState = nip05, targetState = nip05,
modifier = columnModifier, modifier = columnModifier,
label = "ObserveDisplayNip05StatusCrossfade",
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) { ) {
VerifyAndDisplayNIP05OrStatusLine( VerifyAndDisplayNIP05OrStatusLine(
@@ -153,7 +152,6 @@ fun ObserveDisplayNip05Status(
nav, nav,
) )
} }
}
} }
@Composable @Composable
@@ -38,7 +38,6 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -50,9 +49,6 @@ import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync
import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.logTime
@@ -60,7 +56,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.GenericLoadable
@@ -137,7 +133,6 @@ import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing10dp import com.vitorpamplona.amethyst.ui.theme.RowColSpacing10dp
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.Size34dp
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
@@ -1195,15 +1190,8 @@ private fun ChannelNotePicture(
loadProfilePicture: Boolean, loadProfilePicture: Boolean,
loadRobohash: Boolean, loadRobohash: Boolean,
) { ) {
ChannelFinderFilterAssemblerSubscription(baseChannel, Amethyst.instance.sources.channelFinder) val model by observeChannelPicture(baseChannel)
val model by
baseChannel.live
.map { it.channel.profilePicture() }
.distinctUntilChanged()
.observeAsState()
Box(Size30Modifier) {
RobohashFallbackAsyncImage( RobohashFallbackAsyncImage(
robot = baseChannel.idHex, robot = baseChannel.idHex,
model = model, model = model,
@@ -1212,7 +1200,6 @@ private fun ChannelNotePicture(
loadProfilePicture = loadProfilePicture, loadProfilePicture = loadProfilePicture,
loadRobohash = loadRobohash, loadRobohash = loadRobohash,
) )
}
} }
@Composable @Composable
@@ -93,8 +93,6 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
@@ -405,28 +403,6 @@ private fun WatchReactionsZapsBoostsAndDisplayIfExists(
} }
} }
fun <T, K, R> LiveData<T>.combineWith(
liveData1: LiveData<K>,
block: (T?, K?) -> R,
): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(this) { result.value = block(this.value, liveData1.value) }
result.addSource(liveData1) { result.value = block(this.value, liveData1.value) }
return result
}
fun <T, K, P, R> LiveData<T>.combineWith(
liveData1: LiveData<K>,
liveData2: LiveData<P>,
block: (T?, K?, P?) -> R,
): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(this) { result.value = block(this.value, liveData1.value, liveData2.value) }
result.addSource(liveData1) { result.value = block(this.value, liveData1.value, liveData2.value) }
result.addSource(liveData2) { result.value = block(this.value, liveData1.value, liveData2.value) }
return result
}
@Composable @Composable
private fun RenderShowIndividualReactionsButton( private fun RenderShowIndividualReactionsButton(
wantsToSeeReactions: MutableState<Boolean>, wantsToSeeReactions: MutableState<Boolean>,
@@ -223,12 +223,10 @@ class UpdateZapAmountViewModel : ViewModel() {
fun updateNIP47(uri: String) { fun updateNIP47(uri: String) {
val contact = Nip47WalletConnect.parse(uri) val contact = Nip47WalletConnect.parse(uri)
if (contact != null) {
walletConnectPubkey = TextFieldValue(contact.pubKeyHex) walletConnectPubkey = TextFieldValue(contact.pubKeyHex)
walletConnectRelay = TextFieldValue(contact.relayUri ?: "") walletConnectRelay = TextFieldValue(contact.relayUri)
walletConnectSecret = TextFieldValue(contact.secret ?: "") walletConnectSecret = TextFieldValue(contact.secret ?: "")
} }
}
} }
@Composable @Composable
@@ -30,7 +30,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -43,15 +42,16 @@ import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.WatchIsHiddenUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapReqResponse import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapReqResponse
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
@@ -161,13 +161,12 @@ fun UserActionOptions(
baseAuthor: User, baseAuthor: User,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
WatchIsHiddenUser(baseAuthor, accountViewModel) { isHidden -> val isHidden by observeAccountIsHiddenUser(accountViewModel.account, baseAuthor)
if (isHidden) { if (isHidden) {
ShowUserButton { accountViewModel.show(baseAuthor) } ShowUserButton { accountViewModel.show(baseAuthor) }
} else { } else {
ShowFollowingOrUnfollowingButton(baseAuthor, accountViewModel) ShowFollowingOrUnfollowingButton(baseAuthor, accountViewModel)
} }
}
} }
@Composable @Composable
@@ -175,24 +174,9 @@ fun ShowFollowingOrUnfollowingButton(
baseAuthor: User, baseAuthor: User,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
var isFollowing by remember { mutableStateOf(false) } var isFollowing = observeUserIsFollowing(accountViewModel.account.userProfile(), baseAuthor)
val accountFollowsState by accountViewModel.account
.userProfile()
.live()
.follows
.observeAsState()
LaunchedEffect(key1 = accountFollowsState) { if (isFollowing.value) {
launch(Dispatchers.Default) {
val newShowFollowingMark = accountFollowsState?.user?.isFollowing(baseAuthor) == true
if (newShowFollowingMark != isFollowing) {
isFollowing = newShowFollowingMark
}
}
}
if (isFollowing) {
UnfollowButton { UnfollowButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
accountViewModel.toastManager.toast( accountViewModel.toastManager.toast(
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -29,7 +29,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ShowChart import androidx.compose.material.icons.automirrored.filled.ShowChart
import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.ShowChart
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -23,12 +23,7 @@ package com.vitorpamplona.amethyst.ui.note.creators.zapsplits
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import kotlin.collections.all
import kotlin.collections.getOrNull
import kotlin.collections.lastIndex
import kotlin.compareTo
import kotlin.math.abs import kotlin.math.abs
import kotlin.text.toDouble
class SplitBuilder<T> { class SplitBuilder<T> {
var items: List<SplitItem<T>> by mutableStateOf(emptyList()) var items: List<SplitItem<T>> by mutableStateOf(emptyList())
@@ -50,7 +50,7 @@ fun DefaultImageHeader(
) { ) {
WatchAuthor(baseNote = note) { WatchAuthor(baseNote = note) {
Box { Box {
BannerImage(it) BannerImage(it, Modifier.fillMaxWidth().heightIn(max = 200.dp))
Box(authorNotePictureForImageHeader.align(Alignment.BottomStart)) { Box(authorNotePictureForImageHeader.align(Alignment.BottomStart)) {
BaseUserPicture(it, Size55dp, accountViewModel, Modifier) BaseUserPicture(it, Size55dp, accountViewModel, Modifier)
@@ -62,11 +62,19 @@ fun DefaultImageHeader(
@Composable @Composable
fun BannerImage( fun BannerImage(
author: User, author: User,
imageModifier: Modifier = Modifier.fillMaxWidth().heightIn(max = 200.dp), modifier: Modifier = Modifier,
) { ) {
val banner by observeUserBanner(author) val banner by observeUserBanner(author)
if (banner.isNotBlank()) { BannerImage(banner, modifier)
}
@Composable
fun BannerImage(
banner: String?,
modifier: Modifier = Modifier,
) {
if (!banner.isNullOrBlank()) {
AsyncImage( AsyncImage(
model = banner, model = banner,
contentDescription = contentDescription =
@@ -75,7 +83,7 @@ fun BannerImage(
banner, banner,
), ),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = imageModifier, modifier = modifier,
placeholder = painterResource(R.drawable.profile_banner), placeholder = painterResource(R.drawable.profile_banner),
) )
} else { } else {
@@ -83,7 +91,7 @@ fun BannerImage(
painter = painterResource(R.drawable.profile_banner), painter = painterResource(R.drawable.profile_banner),
contentDescription = stringRes(R.string.profile_banner), contentDescription = stringRes(R.string.profile_banner),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = imageModifier, modifier = modifier,
) )
} }
} }
@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.ui.note.elements package com.vitorpamplona.amethyst.ui.note.elements
import android.R.attr.maxLines
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.ui.note.elements package com.vitorpamplona.amethyst.ui.note.elements
import android.R.attr.maxLines
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -30,7 +30,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -42,6 +41,8 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBookmarks
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollows
import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.actions.EditPostView
import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.GenericLoadable
@@ -59,6 +60,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable @Composable
fun MoreOptionsButton( fun MoreOptionsButton(
@@ -380,22 +382,12 @@ fun WatchBookmarksFollowsAndAccount(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onNew: (DropDownParams) -> Unit, onNew: (DropDownParams) -> Unit,
) { ) {
val followState by accountViewModel val followState by observeUserFollows(accountViewModel.userProfile())
.userProfile() val bookmarkState by observeUserBookmarks(accountViewModel.userProfile())
.live() val showSensitiveContent by accountViewModel.showSensitiveContent().collectAsStateWithLifecycle()
.follows
.observeAsState()
val bookmarkState by accountViewModel
.userProfile()
.live()
.bookmarks
.observeAsState()
val showSensitiveContent by accountViewModel
.showSensitiveContent()
.collectAsStateWithLifecycle()
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) { LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) {
launch(Dispatchers.IO) { withContext(Dispatchers.IO) {
accountViewModel.isInPrivateBookmarks(note) { accountViewModel.isInPrivateBookmarks(note) {
val newState = val newState =
DropDownParams( DropDownParams(
@@ -212,9 +212,7 @@ fun LongCommunityHeader(
if (participants != null) { if (participants != null) {
accountViewModel.loadParticipants(participants) { newParticipantUsers -> accountViewModel.loadParticipants(participants) { newParticipantUsers ->
if ( if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers)
) {
participantUsers = newParticipantUsers participantUsers = newParticipantUsers
} }
} }
@@ -30,9 +30,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -45,6 +43,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserRelayIntoList
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
@@ -279,21 +278,7 @@ private fun RelayOptionsAction(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val userStateRelayInfo by accountViewModel.account val isCurrentlyOnTheUsersList by observeUserRelayIntoList(accountViewModel.userProfile(), relay)
.userProfile()
.live()
.relayInfo
.observeAsState()
val isCurrentlyOnTheUsersList by
remember(userStateRelayInfo) {
derivedStateOf {
userStateRelayInfo
?.user
?.latestContactList
?.relays()
?.none { it.key == relay } == true
}
}
if (isCurrentlyOnTheUsersList) { if (isCurrentlyOnTheUsersList) {
AddRelayButton { AddRelayButton {
@@ -22,8 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen
import android.util.Log import android.util.Log
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
@@ -33,10 +33,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
@@ -67,7 +67,7 @@ fun BookmarkListScreen(
factory = BookmarkPrivateFeedViewModel.Factory(accountViewModel.account), factory = BookmarkPrivateFeedViewModel.Factory(accountViewModel.account),
) )
val userState by accountViewModel.account.decryptBookmarks.observeAsState() val userState by accountViewModel.account.decryptBookmarks.collectAsStateWithLifecycle(null)
LaunchedEffect(userState) { LaunchedEffect(userState) {
publicFeedViewModel.invalidateData() publicFeedViewModel.invalidateData()
@@ -60,7 +60,6 @@ class ChatroomFilterAssembler(
) )
fun createMessagesFromMeFilter(key: ChatroomQueryState): TypedFilter? = fun createMessagesFromMeFilter(key: ChatroomQueryState): TypedFilter? =
if (key.room != null) {
TypedFilter( TypedFilter(
types = setOf(FeedType.PRIVATE_DMS), types = setOf(FeedType.PRIVATE_DMS),
filter = filter =
@@ -75,9 +74,6 @@ class ChatroomFilterAssembler(
?.relayList, ?.relayList,
), ),
) )
} else {
null
}
fun clearEOSEs(account: Account) { fun clearEOSEs(account: Account) {
latestEOSEs.removeDataFor(account.userProfile()) latestEOSEs.removeDataFor(account.userProfile())
@@ -24,14 +24,12 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserRoomSubject
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserShortName import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserShortName
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@@ -49,14 +47,7 @@ fun RoomNameOnlyDisplay(
fontWeight: FontWeight = FontWeight.Bold, fontWeight: FontWeight = FontWeight.Bold,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val roomSubject by val roomSubject by observeUserRoomSubject(accountViewModel.userProfile(), room)
accountViewModel
.userProfile()
.live()
.messages
.map { it.user.privateChatrooms[room]?.subject }
.distinctUntilChanged()
.observeAsState(accountViewModel.userProfile().privateChatrooms[room]?.subject)
CrossfadeIfEnabled(targetState = roomSubject, modifier, accountViewModel = accountViewModel) { CrossfadeIfEnabled(targetState = roomSubject, modifier, accountViewModel = accountViewModel) {
if (!it.isNullOrBlank()) { if (!it.isNullOrBlank()) {
@@ -107,16 +98,9 @@ fun RoomNameDisplay(
modifier: Modifier, modifier: Modifier,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val roomSubject by val roomSubject by observeUserRoomSubject(accountViewModel.userProfile(), room)
accountViewModel
.userProfile()
.live()
.messages
.map { it.user.privateChatrooms[room]?.subject }
.distinctUntilChanged()
.observeAsState(accountViewModel.userProfile().privateChatrooms[room]?.subject)
CrossfadeIfEnabled(targetState = roomSubject, modifier, label = "RoomNameDisplay", accountViewModel = accountViewModel) { CrossfadeIfEnabled(targetState = roomSubject, modifier, accountViewModel = accountViewModel) {
if (!it.isNullOrBlank()) { if (!it.isNullOrBlank()) {
if (room.users.size > 1) { if (room.users.size > 1) {
DisplayRoomSubject(it) DisplayRoomSubject(it)
@@ -28,7 +28,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -21,8 +21,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload
import android.content.Context import android.content.Context
import androidx.core.app.PendingIntentCompat.send
import coil3.util.CoilUtils.result
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
@@ -32,7 +32,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -41,12 +40,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
@@ -67,7 +65,6 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
@Composable @Composable
fun LongPublicChatChannelHeader( fun LongPublicChatChannelHeader(
@@ -181,44 +178,42 @@ fun LongChannelActionOptions(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val isMe by
remember(accountViewModel) {
derivedStateOf { channel.creator == accountViewModel.account.userProfile() }
}
OpenChatButton(channel, accountViewModel, nav) OpenChatButton(channel, accountViewModel, nav)
LinkChatButton(channel, accountViewModel, nav) LinkChatButton(channel, accountViewModel, nav)
ShareChatButton(channel, accountViewModel, nav) ShareChatButton(channel, accountViewModel, nav)
if (isMe) { EditButtonIfIamCreator(channel, accountViewModel, nav)
EditButton(channel, accountViewModel, nav)
LeaveButtonIfFollowing(channel, accountViewModel, nav)
}
@Composable
fun EditButtonIfIamCreator(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val isMe by
remember(accountViewModel) {
derivedStateOf { channel.creator == accountViewModel.account.userProfile() }
} }
WatchChannelFollows(channel, accountViewModel) { isFollowing -> if (isMe) {
if (isFollowing) { EditButton(channel, accountViewModel, nav)
LeaveChatButton(channel, accountViewModel, nav)
}
} }
} }
@Composable @Composable
fun WatchChannelFollows( fun LeaveButtonIfFollowing(
channel: PublicChatChannel, channel: PublicChatChannel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
content: @Composable (Boolean) -> Unit, nav: INav,
) { ) {
val isFollowing by val isFollowing by observeUserIsFollowingChannel(accountViewModel.userProfile(), channel)
accountViewModel
.userProfile()
.live()
.follows
.map { it.user.latestContactList?.isTaggedEvent(channel.idHex) ?: false }
.distinctUntilChanged()
.observeAsState(
accountViewModel.userProfile().latestContactList?.isTaggedEvent(channel.idHex) ?: false,
)
content(isFollowing) if (isFollowing) {
LeaveChatButton(channel, accountViewModel, nav)
}
} }
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.popBack
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
@@ -129,9 +130,18 @@ fun ShortChannelActionOptions(
} }
} }
WatchChannelFollows(channel, accountViewModel) { isFollowing -> JoinChatButtonIfNotAlreadyJoined(channel, accountViewModel, nav)
}
@Composable
fun JoinChatButtonIfNotAlreadyJoined(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val isFollowing by observeUserIsFollowingChannel(accountViewModel.userProfile(), channel)
if (!isFollowing) { if (!isFollowing) {
JoinChatButton(channel, accountViewModel, nav) JoinChatButton(channel, accountViewModel, nav)
} }
}
} }
@@ -27,13 +27,10 @@ import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -28,8 +28,6 @@ import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -51,9 +51,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.isNotEmpty
import kotlin.collections.map
import kotlin.collections.plus
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
class ChannelMetadataViewModel : ViewModel() { class ChannelMetadataViewModel : ViewModel() {
@@ -24,15 +24,12 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelInfo
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -45,47 +42,48 @@ fun ShowVideoStreaming(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
baseChannel.info?.let { baseChannel.info?.let {
SensitivityWarning( val streamingInfoEvent by observeChannelInfo(baseChannel)
event = it,
accountViewModel = accountViewModel,
) {
ChannelFinderFilterAssemblerSubscription(baseChannel, accountViewModel.dataSources().channelFinder)
val streamingInfoEvent by
baseChannel.live
.map {
(it.channel as? LiveActivitiesChannel)?.info
}.distinctUntilChanged()
.observeAsState(baseChannel.info)
streamingInfoEvent?.let { event -> streamingInfoEvent?.let { event ->
event.streaming()?.let { url -> event.streaming()?.let { url ->
CrossfadeCheckIfVideoIsOnline(url, accountViewModel) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = StreamingHeaderModifier,
) {
val zoomableUrlVideo = val zoomableUrlVideo =
remember(streamingInfoEvent) { remember(streamingInfoEvent) {
MediaUrlVideo( MediaUrlVideo(
url = url, url = url,
description = baseChannel.toBestDisplayName(), description = event.title() ?: baseChannel.toBestDisplayName(),
artworkUri = event.image(), artworkUri = event.image(),
authorName = baseChannel.creatorName(), authorName = baseChannel.creatorName(),
uri = baseChannel.toNAddr(), uri = baseChannel.toNAddr(),
) )
} }
SensitivityWarning(
event = event,
accountViewModel = accountViewModel,
) {
RenderStreaming(zoomableUrlVideo, accountViewModel)
}
}
}
}
}
@Composable
private fun RenderStreaming(
media: MediaUrlVideo,
accountViewModel: AccountViewModel,
) {
CrossfadeCheckIfVideoIsOnline(media.url, accountViewModel) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = StreamingHeaderModifier,
) {
ZoomableContentView( ZoomableContentView(
content = zoomableUrlVideo, content = media,
roundedCorner = false, roundedCorner = false,
contentScale = ContentScale.FillWidth, contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} }
} }
}
}
}
}
} }
@@ -48,7 +48,6 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.navigation.Route.NewGroupDM import com.vitorpamplona.amethyst.ui.navigation.Route.NewGroupDM
@@ -71,7 +71,6 @@ import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -30,7 +30,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import kotlin.collections.get
class ChatroomListKnownFeedFilter( class ChatroomListKnownFeedFilter(
val account: Account, val account: Account,
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import kotlin.collections.get
class ChatroomListNewFeedFilter( class ChatroomListNewFeedFilter(
val account: Account, val account: Account,
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -28,7 +28,6 @@ import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
import kotlin.collections.ifEmpty
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class CommunityQueryState( class CommunityQueryState(
@@ -43,7 +43,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.flatten
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class DiscoveryQueryState( class DiscoveryQueryState(
@@ -24,14 +24,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingGeohash
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
@@ -123,15 +121,7 @@ fun GeoHashActionOptions(
tag: String, tag: String,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val userState by accountViewModel val isFollowingTag by observeUserIsFollowingGeohash(accountViewModel.userProfile(), tag)
.userProfile()
.live()
.follows
.observeAsState()
val isFollowingTag by
remember(userState, tag) {
derivedStateOf { userState?.user?.isFollowingGeohash(tag) ?: false }
}
if (isFollowingTag) { if (isFollowingTag) {
UnfollowButton { UnfollowButton {
@@ -28,15 +28,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingHashtag
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
@@ -140,15 +138,7 @@ fun HashtagActionOptions(
tag: String, tag: String,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val userState by accountViewModel val isFollowingTag by observeUserIsFollowingHashtag(accountViewModel.userProfile(), tag)
.userProfile()
.live()
.follows
.observeAsState()
val isFollowingTag by
remember(userState, tag) {
derivedStateOf { userState?.user?.isFollowingHashtag(tag) ?: false }
}
if (isFollowingTag) { if (isFollowingTag) {
UnfollowButton { UnfollowButton {
@@ -37,7 +37,6 @@ import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessa
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
import kotlin.collections.flatten
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class HashtagQueryState( class HashtagQueryState(
@@ -52,7 +52,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.flatten
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class HomeQueryState( class HomeQueryState(
@@ -43,7 +43,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -58,11 +57,11 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -400,7 +399,10 @@ private fun RenderScreen(
modifier = tabRowModifier, modifier = tabRowModifier,
divider = { HorizontalDivider(thickness = DividerThickness) }, divider = { HorizontalDivider(thickness = DividerThickness) },
) { ) {
CreateAndRenderTabs(baseUser, pagerState) CreateAndRenderTabs(
baseUser,
pagerState,
)
} }
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
@@ -470,11 +472,7 @@ fun UpdateThreadsAndRepliesWhenBlockUnblock(
repliesViewModel: UserProfileConversationsFeedViewModel, repliesViewModel: UserProfileConversationsFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val isHidden by val isHidden by observeAccountIsHiddenUser(accountViewModel.account, baseUser)
accountViewModel.account.liveHiddenUsers
.map {
it.hiddenUsers.contains(baseUser.pubkeyHex) || it.spammers.contains(baseUser.pubkeyHex)
}.observeAsState(accountViewModel.account.isHidden(baseUser))
LaunchedEffect(key1 = isHidden) { LaunchedEffect(key1 = isHidden) {
threadsViewModel.invalidateData() threadsViewModel.invalidateData()
@@ -22,33 +22,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBookmarkCount
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun BookmarkTabHeader(baseUser: User) { fun BookmarkTabHeader(baseUser: User) {
val userState by baseUser.live().bookmarks.observeAsState() val userBookmarks by observeUserBookmarkCount(baseUser)
var userBookmarks by remember { mutableIntStateOf(0) }
LaunchedEffect(key1 = userState) {
launch(Dispatchers.IO) {
val newBookmarks = userState?.user?.latestBookmarkList?.countBookmarks() ?: 0
if (newBookmarks != userBookmarks) {
userBookmarks = newBookmarks
}
}
}
Text(text = "$userBookmarks ${stringRes(R.string.bookmarks)}") Text(text = "$userBookmarks ${stringRes(R.string.bookmarks)}")
} }
@@ -52,7 +52,6 @@ import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent
import kotlin.collections.flatten
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class UserProfileQueryState( class UserProfileQueryState(
@@ -22,34 +22,22 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowerCount
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun FollowersTabHeader(baseUser: User) { fun FollowersTabHeader(baseUser: User) {
val userState by baseUser.live().followers.observeAsState() val followerCount by observeUserFollowerCount(baseUser)
var followerCount by remember { mutableStateOf("--") }
val text = stringRes(R.string.followers) val text =
if (followerCount > 0) {
LaunchedEffect(key1 = userState) { stringRes(R.string.number_followers, followerCount)
launch(Dispatchers.IO) { } else {
val newFollower = (userState?.user?.transientFollowerCount()?.toString() ?: "--") + " " + text stringRes(R.string.number_followers, "--")
if (followerCount != newFollower) {
followerCount = newFollower
}
}
} }
Text(text = followerCount) Text(text = text)
} }
@@ -25,9 +25,9 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowers
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
@@ -52,7 +52,7 @@ private fun WatchFollowerChanges(
baseUser: User, baseUser: User,
feedViewModel: UserFeedViewModel, feedViewModel: UserFeedViewModel,
) { ) {
val userState by baseUser.live().followers.observeAsState() val userState by observeUserFollowers(baseUser)
LaunchedEffect(userState) { feedViewModel.invalidateData() } LaunchedEffect(userState) { feedViewModel.invalidateData() }
} }
@@ -22,34 +22,22 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowCount
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun FollowTabHeader(baseUser: User) { fun FollowTabHeader(baseUser: User) {
val userState by baseUser.live().follows.observeAsState() val followCount by observeUserFollowCount(baseUser)
var followCount by remember { mutableStateOf("--") }
val text = stringRes(R.string.follows) val text =
if (followCount > 0) {
LaunchedEffect(key1 = userState) { stringRes(R.string.number_following, followCount)
launch(Dispatchers.IO) { } else {
val newFollow = (userState?.user?.transientFollowCount()?.toString() ?: "--") + " " + text stringRes(R.string.number_following, "--")
if (followCount != newFollow) {
followCount = newFollow
}
}
} }
Text(text = followCount) Text(text = text)
} }
@@ -25,9 +25,9 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollows
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
@@ -52,7 +52,7 @@ private fun WatchFollowChanges(
baseUser: User, baseUser: User,
feedViewModel: UserFeedViewModel, feedViewModel: UserFeedViewModel,
) { ) {
val userState by baseUser.live().follows.observeAsState() val userState by observeUserFollows(baseUser)
LaunchedEffect(userState) { feedViewModel.invalidateData() } LaunchedEffect(userState) { feedViewModel.invalidateData() }
} }
@@ -33,7 +33,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.FeedError
@@ -22,23 +22,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserTagFollows
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@Composable @Composable
fun FollowedTagsTabHeader(baseUser: User) { fun FollowedTagsTabHeader(baseUser: User) {
val userState by baseUser.live().follows.observeAsState() val usertags by observeUserTagFollows(baseUser)
val usertags by remember(baseUser) {
derivedStateOf {
userState?.user?.latestContactList?.countFollowTags() ?: 0
}
}
Text(text = "$usertags ${stringRes(R.string.followed_tags)}") Text(text = "$usertags ${stringRes(R.string.followed_tags)}")
} }
@@ -22,11 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton
@@ -36,22 +34,8 @@ fun DisplayFollowUnfollowButton(
baseUser: User, baseUser: User,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val isLoggedInFollowingUser by val isLoggedInFollowingUser by observeUserIsFollowing(accountViewModel.account.userProfile(), baseUser)
accountViewModel.account val isUserFollowingLoggedIn by observeUserIsFollowing(baseUser, accountViewModel.account.userProfile())
.userProfile()
.live()
.follows
.map { it.user.isFollowing(baseUser) }
.distinctUntilChanged()
.observeAsState(initial = accountViewModel.account.isFollowing(baseUser))
val isUserFollowingLoggedIn by
baseUser
.live()
.follows
.map { it.user.isFollowing(accountViewModel.account.userProfile()) }
.distinctUntilChanged()
.observeAsState(initial = baseUser.isFollowing(accountViewModel.account.userProfile()))
if (isLoggedInFollowingUser) { if (isLoggedInFollowingUser) {
UnfollowButton { UnfollowButton {
@@ -53,10 +53,19 @@ fun DrawBanner(
) { ) {
val banner by observeUserBanner(baseUser) val banner by observeUserBanner(baseUser)
DrawBanner(banner, accountViewModel)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DrawBanner(
banner: String?,
accountViewModel: AccountViewModel,
) {
if (!banner.isNullOrBlank()) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
var zoomImageDialogOpen by remember { mutableStateOf(false) } var zoomImageDialogOpen by remember { mutableStateOf(false) }
if (banner.isNotBlank()) {
AsyncImage( AsyncImage(
model = banner, model = banner,
contentDescription = stringRes(id = R.string.profile_image), contentDescription = stringRes(id = R.string.profile_image),
@@ -25,10 +25,10 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.WatchIsHiddenUser
@Composable @Composable
fun ProfileActions( fun ProfileActions(
@@ -43,11 +43,11 @@ fun ProfileActions(
EditButton(nav) EditButton(nav)
} }
WatchIsHiddenUser(baseUser, accountViewModel) { isHidden -> val isHidden by observeAccountIsHiddenUser(accountViewModel.account, baseUser)
if (isHidden) { if (isHidden) {
ShowUserButton { accountViewModel.showUser(baseUser.pubkeyHex) } ShowUserButton { accountViewModel.showUser(baseUser.pubkeyHex) }
} else { } else {
DisplayFollowUnfollowButton(baseUser, accountViewModel) DisplayFollowUnfollowButton(baseUser, accountViewModel)
} }
}
} }
@@ -28,13 +28,16 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.model.RelayInfo
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -51,55 +54,72 @@ class RelayFeedViewModel :
val feedContent = _feedContent.asStateFlow() val feedContent = _feedContent.asStateFlow()
var currentUser: User? = null var currentUser: User? = null
var currentJob: Job? = null
override val isRefreshing: MutableState<Boolean> = mutableStateOf(false) override val isRefreshing: MutableState<Boolean> = mutableStateOf(false)
fun refresh() { fun refresh() {
viewModelScope.launch(Dispatchers.Default) { refreshSuspended() } viewModelScope.launch(Dispatchers.Default) {
refreshSuspended()
}
} }
fun refreshSuspended() { fun refreshSuspended() {
try { try {
isRefreshing.value = true isRefreshing.value = true
val beingUsed = currentUser?.relaysBeingUsed?.values ?: emptyList() currentUser?.let {
val beingUsedSet = currentUser?.relaysBeingUsed?.keys ?: emptySet() val newList = mergeRelays(it.relaysBeingUsed, it.latestContactList?.relays())
val newRelaysFromRecord =
currentUser?.latestContactList?.relays()?.entries?.mapNotNullTo(HashSet()) {
val url = RelayUrlFormatter.normalize(it.key)
if (url !in beingUsedSet) {
RelayInfo(url, 0, 0)
} else {
null
}
}
?: emptyList()
val newList = (beingUsed + newRelaysFromRecord).sortedWith(order)
_feedContent.update { newList } _feedContent.update { newList }
}
} finally { } finally {
isRefreshing.value = false isRefreshing.value = false
} }
} }
val listener: (UserState) -> Unit = { invalidateData() } fun mergeRelays(
relaysBeingUsed: Map<String, RelayInfo>,
relays: Map<String, ReadWrite>?,
): List<RelayInfo> {
val userRelaysBeingUsed = relaysBeingUsed.map { it.value }
val currentUserRelays =
relays?.mapNotNull {
val url = RelayUrlFormatter.normalize(it.key)
if (url !in relaysBeingUsed) {
RelayInfo(url, 0, 0)
} else {
null
}
} ?: emptyList()
return (userRelaysBeingUsed + currentUserRelays).sortedWith(order)
}
fun subscribeTo(user: User) { fun subscribeTo(user: User) {
if (currentUser != user) { if (currentUser != user) {
currentUser = user currentUser = user
user.live().relays.observeForever(listener)
user.live().relayInfo.observeForever(listener) currentJob?.cancel()
currentJob =
viewModelScope.launch {
combine(currentUser!!.flow().relays.stateFlow, currentUser!!.flow().relayInfo.stateFlow) { relays, relayInfo ->
mergeRelays(relays.user.relaysBeingUsed, relayInfo.user.latestContactList?.relays())
}.debounce(1000)
.collect { newList ->
_feedContent.update { newList }
}
}
invalidateData() invalidateData()
} }
} }
fun unsubscribeTo(user: User) { fun unsubscribeTo(user: User) {
if (currentUser == user) { if (currentUser == user) {
user.live().relays.removeObserver(listener)
user.live().relayInfo.removeObserver(listener)
currentUser = null currentUser = null
currentJob?.cancel()
invalidateData()
} }
} }
@@ -116,6 +136,7 @@ class RelayFeedViewModel :
override fun onCleared() { override fun onCleared() {
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
bundler.cancel() bundler.cancel()
currentJob?.cancel()
super.onCleared() super.onCleared()
} }
} }
@@ -23,26 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserRelaysUsing
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@Composable @Composable
fun RelaysTabHeader(baseUser: User) { fun RelaysTabHeader(baseUser: User) {
val userState by baseUser.live().relays.observeAsState() val userState by observeUserRelaysUsing(baseUser)
val userRelaysBeingUsed = remember(userState) { userState?.user?.relaysBeingUsed?.size ?: "--" }
val userStateRelayInfo by baseUser.live().relayInfo.observeAsState() Text(text = "${sizeAsString(userState.userRelayList.size)} / ${sizeAsString(userState.relays.size)} ${stringRes(R.string.relays)}")
val userRelays =
remember(userStateRelayInfo) {
userStateRelayInfo
?.user
?.latestContactList
?.relays()
?.size ?: "--"
}
Text(text = "$userRelaysBeingUsed / $userRelays ${stringRes(R.string.relays)}")
} }
private fun sizeAsString(count: Int) = if (count > 0) count.toString() else "--"
@@ -22,33 +22,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserReportCount
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun ReportsTabHeader(baseUser: User) { fun ReportsTabHeader(baseUser: User) {
val userState by baseUser.live().reports.observeAsState() val reportCount by observeUserReportCount(baseUser)
var userReports by remember { mutableIntStateOf(0) }
LaunchedEffect(key1 = userState) { if (reportCount > 0) {
launch(Dispatchers.IO) { Text(text = stringRes(R.string.number_reports, reportCount))
val newSize = UserProfileReportsFeedFilter(baseUser).feed().size } else {
Text(text = stringRes(R.string.reports))
if (newSize != userReports) {
userReports = newSize
} }
}
}
Text(text = "$userReports ${stringRes(R.string.reports)}")
} }
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserReports
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel
@Composable @Composable
@@ -32,6 +32,6 @@ fun WatchReportsAndUpdateFeed(
baseUser: User, baseUser: User,
feedViewModel: UserProfileReportFeedViewModel, feedViewModel: UserProfileReportFeedViewModel,
) { ) {
val userState by baseUser.live().reports.observeAsState() val userState by observeUserReports(baseUser)
LaunchedEffect(userState) { feedViewModel.invalidateData() } LaunchedEffect(userState) { feedViewModel.invalidateData() }
} }
@@ -25,7 +25,6 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCache.notes
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
@@ -1,43 +0,0 @@
/**
* Copyright (c) 2024 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.profile.zaps
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun WatchIsHiddenUser(
baseUser: User,
accountViewModel: AccountViewModel,
content: @Composable (Boolean) -> Unit,
) {
val isHidden by
accountViewModel.account.liveHiddenUsers
.map {
it.hiddenUsers.contains(baseUser.pubkeyHex) || it.spammers.contains(baseUser.pubkeyHex)
}.observeAsState(accountViewModel.account.isHidden(baseUser))
content(isHidden)
}
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserZaps
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel
@Composable @Composable
@@ -32,7 +32,7 @@ fun WatchZapsAndUpdateFeed(
baseUser: User, baseUser: User,
feedViewModel: UserProfileZapsFeedViewModel, feedViewModel: UserProfileZapsFeedViewModel,
) { ) {
val userState by baseUser.live().zaps.observeAsState() val userState by observeUserZaps(baseUser)
LaunchedEffect(userState) { feedViewModel.invalidateData() } LaunchedEffect(userState) { feedViewModel.invalidateData() }
} }
@@ -22,33 +22,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserZapAmount
import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.note.showAmountInteger
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.math.BigDecimal
@Composable @Composable
fun ZapTabHeader(baseUser: User) { fun ZapTabHeader(baseUser: User) {
val userState by baseUser.live().zaps.observeAsState() val zapAmount by observeUserZapAmount(baseUser)
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
LaunchedEffect(key1 = userState) {
launch(Dispatchers.Default) {
val tempAmount = baseUser.zappedAmount()
if (zapAmount != tempAmount) {
zapAmount = tempAmount
}
}
}
Text(text = "${showAmountInteger(zapAmount)} ${stringRes(id = R.string.zaps)}") Text(text = "${showAmountInteger(zapAmount)} ${stringRes(id = R.string.zaps)}")
} }
@@ -42,7 +42,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.plus
@Stable @Stable
class Kind3RelayListViewModel : ViewModel() { class Kind3RelayListViewModel : ViewModel() {
@@ -46,7 +46,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -63,10 +62,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenWord
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
@@ -323,11 +321,7 @@ fun MutedWordActionOptions(
word: String, word: String,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val isMutedWord by val isMutedWord by observeAccountIsHiddenWord(accountViewModel.account, word)
accountViewModel.account.liveHiddenUsers
.map { word in it.hiddenWords }
.distinctUntilChanged()
.observeAsState()
if (isMutedWord == true) { if (isMutedWord == true) {
ShowWordButton { ShowWordButton {
@@ -28,8 +28,6 @@ import com.vitorpamplona.ammolite.relays.NostrClient
import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlin.collections.flatten
import kotlin.collections.ifEmpty
// This allows multiple screen to be listening to tags, even the same tag // This allows multiple screen to be listening to tags, even the same tag
class ThreadQueryState( class ThreadQueryState(
@@ -39,7 +39,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.flatten
val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif") val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif")
val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet() val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet()
@@ -26,7 +26,6 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import org.torproject.jni.TorService
/** /**
* There should be only one instance of the Tor binding per app. * There should be only one instance of the Tor binding per app.

Some files were not shown because too many files have changed in this diff Show More