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