From 067f353e506b2ed1bb399a95962301b6a0ad1f01 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 24 Apr 2025 14:35:22 -0400 Subject: [PATCH] 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 --- amethyst/build.gradle | 4 - .../com/vitorpamplona/amethyst/DebugUtils.kt | 24 +- .../vitorpamplona/amethyst/model/Account.kt | 29 +- .../vitorpamplona/amethyst/model/Channel.kt | 27 +- .../vitorpamplona/amethyst/model/Chatroom.kt | 2 +- .../amethyst/model/LocalCache.kt | 29 +- .../com/vitorpamplona/amethyst/model/Note.kt | 143 ++--- .../com/vitorpamplona/amethyst/model/User.kt | 126 +---- .../service/Nip11RelayInfoRetriever.kt | 1 - .../service/location/LocationState.kt | 1 - .../service/notifications/PokeyReceiver.kt | 2 - .../relayClient/KeyDataSourceSubscription.kt | 20 +- .../authCommand/model/AuthCoordinator.kt | 1 - .../model/ListWithUniqueSetCache.kt | 2 +- .../QueryBasedSubscriptionOrchestrator.kt | 10 - .../reqCommand/account/AccountObservers.kt | 62 +++ .../channel/ChannelFinderFilterAssembler.kt | 2 - .../reqCommand/channel/ChannelObservers.kt | 43 +- .../event/EventFinderFilterAssembler.kt | 1 - .../reqCommand/event/EventObservers.kt | 215 ++++--- .../reqCommand/user/UserObservers.kt | 525 ++++++++++++++++-- ...tableQueryBasedSubscriptionOrchestrator.kt | 10 - .../searchCommand/SearchFilterAssembler.kt | 1 - .../amethyst/ui/actions/MediaSaverToDisk.kt | 1 + .../ui/actions/NewUserMetadataViewModel.kt | 1 - .../amethyst/ui/components/ClickableUrl.kt | 2 - .../ui/components/ZoomableContentDialog.kt | 1 - .../multiline/MultiUserErrorMessageDialog.kt | 1 - .../ui/components/util/DeviceUtils.kt | 1 + .../amethyst/ui/dal/DefaultFeedOrder.kt | 15 +- .../amethyst/ui/navigation/DrawerContent.kt | 32 +- .../amethyst/ui/navigation/Routes.kt | 1 - .../amethyst/ui/note/BlockReportChecker.kt | 2 - .../vitorpamplona/amethyst/ui/note/Icons.kt | 2 - .../vitorpamplona/amethyst/ui/note/Loaders.kt | 18 +- .../ui/note/NIP05VerificationDisplay.kt | 32 +- .../amethyst/ui/note/NoteCompose.kt | 33 +- .../amethyst/ui/note/ReactionsRow.kt | 24 - .../amethyst/ui/note/UpdateZapAmountDialog.kt | 8 +- .../amethyst/ui/note/ZapNoteCompose.kt | 34 +- .../location/DisplayLocationObserver.kt | 1 - .../creators/messagefield/MessageField.kt | 1 - .../creators/zapraiser/AddZapraiserButton.kt | 1 - .../note/creators/zapsplits/SplitBuilder.kt | 5 - .../ui/note/elements/DefaultImageHeader.kt | 18 +- .../ui/note/elements/DisplayCommunity.kt | 1 - .../ui/note/elements/DisplayHashtags.kt | 1 - .../amethyst/ui/note/elements/DropDownMenu.kt | 22 +- .../amethyst/ui/note/types/CommunityHeader.kt | 4 +- .../amethyst/ui/note/types/RelayList.kt | 19 +- .../amethyst/ui/screen/FeedViewModel.kt | 2 - .../loggedIn/bookmarks/BookmarkListScreen.kt | 4 +- .../datasource/ChatroomFilterAssembler.kt | 32 +- .../privateDM/header/RoomNameOnlyDisplay.kt | 24 +- .../send/PrivateMessageEditFieldRow.kt | 1 - .../privateDM/send/upload/ChatFileUploader.kt | 2 - .../header/LongPublicChatChannelHeader.kt | 51 +- .../header/PublicChatTopBar.kt | 1 - .../header/ShortPublicChatChannelHeader.kt | 18 +- .../header/actions/EditChatButton.kt | 3 - .../header/actions/OpenChatButton.kt | 1 - .../header/actions/ShareChatButton.kt | 2 - .../metadata/ChannelMetadataViewModel.kt | 3 - .../nip53LiveActivities/ShowVideoStreaming.kt | 82 ++- .../loggedIn/chats/rooms/ChannelFabColumn.kt | 1 - .../chats/rooms/ChatroomHeaderCompose.kt | 1 - .../rooms/dal/ChatroomListKnownFeedFilter.kt | 1 - .../rooms/dal/ChatroomListNewFeedFilter.kt | 1 - .../chats/rooms/twopane/TwoPaneNav.kt | 1 - .../datasource/CommunityFilterAssembler.kt | 1 - .../datasource/DiscoveryFilterAssembler.kt | 1 - .../screen/loggedIn/geohash/GeoHashScreen.kt | 14 +- .../screen/loggedIn/hashtag/HashtagScreen.kt | 14 +- .../datasource/HashtagFilterAssembler.kt | 1 - .../home/datasource/HomeFilterAssembler.kt | 1 - .../screen/loggedIn/profile/ProfileScreen.kt | 14 +- .../profile/bookmarks/BookmarkTabHeader.kt | 22 +- .../datasource/UserProfileFilterAssembler.kt | 1 - .../profile/followers/FollowersTabHeader.kt | 28 +- .../profile/followers/TabFollowers.kt | 4 +- .../profile/follows/FollowTabHeader.kt | 28 +- .../loggedIn/profile/follows/TabFollows.kt | 4 +- .../profile/gallery/ProfileGalleryFeed.kt | 1 - .../profile/hashtags/FollowedTagsTabHeader.kt | 12 +- .../header/DisplayFollowUnfollowButton.kt | 22 +- .../loggedIn/profile/header/DrawBanner.kt | 15 +- .../loggedIn/profile/header/ProfileActions.kt | 14 +- .../profile/relays/RelayFeedViewModel.kt | 69 ++- .../profile/relays/RelaysTabHeader.kt | 20 +- .../profile/reports/ReportsTabHeader.kt | 26 +- .../reports/WatchReportsAndUpdateFeed.kt | 4 +- .../profile/zaps/LnZapFeedViewModel.kt | 1 - .../profile/zaps/WatchIsHiddenUser.kt | 43 -- .../profile/zaps/WatchZapsAndUpdateFeed.kt | 4 +- .../loggedIn/profile/zaps/ZapTabHeader.kt | 21 +- .../relays/kind3/Kind3RelayListViewModel.kt | 1 - .../settings/SecurityFiltersScreen.kt | 10 +- .../datasources/ThreadFilterAssembler.kt | 2 - .../video/datasource/VideoFilterAssembler.kt | 1 - .../amethyst/ui/tor/TorManager.kt | 1 - amethyst/src/main/res/values/strings.xml | 4 + .../components/TranslatableRichTextViewer.kt | 3 - .../commons/preview/MetaTagsParser.kt | 44 +- gradle/libs.versions.toml | 2 - 104 files changed, 1166 insertions(+), 1079 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchIsHiddenUser.kt diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 8a22cb0f6..38147f0ca 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 8f8545936..2b04f99f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -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 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()) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index b5160f609..64cd2089d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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 = userProfile().flow().follows.stateFlow.transformLatest { - checkNotInMainThread() emit(buildFollowLists(it.user.latestContactList)) } @@ -1090,24 +1086,25 @@ class Account( ) } - val liveHiddenUsers = flowHiddenUsers.asLiveData() - - val decryptBookmarks: LiveData by lazy { - userProfile().live().bookmarks.switchMap { userState -> - liveData(Dispatchers.IO) { + val decryptBookmarks: Flow 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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 15e60c193..7d281052e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -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 { val important = @@ -257,22 +256,24 @@ abstract class Channel( } } -class ChannelLiveData( +class ChannelFlow( val channel: Channel, -) : LiveData(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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt index 77b71ebe5..419216cdd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 9486ebd0a..18aaceaf7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -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() 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 7fb9825e7..e511cd844 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -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() 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() = @@ -980,33 +962,11 @@ class NoteFlowSet( ?.stateFlow ?: MutableStateFlow(null) } - 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() || replies.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(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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index d79f81f3b..906735b8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -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(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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt index ec9567f49..172dd0cf2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt index 18573b4f1..859692bf6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt index abdddba9e..348a7cd73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt index 5d62b1744..d42614c46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/KeyDataSourceSubscription.kt @@ -30,12 +30,10 @@ import com.vitorpamplona.amethyst.service.relayClient.searchCommand.MutableQuery fun KeyDataSourceSubscription( state: T, dataSource: QueryBasedSubscriptionOrchestrator, -) { - DisposableEffect(state) { - dataSource.subscribe(state) - onDispose { - dataSource.unsubscribe(state) - } +) = DisposableEffect(state) { + dataSource.subscribe(state) + onDispose { + dataSource.unsubscribe(state) } } @@ -43,11 +41,9 @@ fun KeyDataSourceSubscription( fun KeyDataSourceSubscription( state: T, dataSource: MutableQueryBasedSubscriptionOrchestrator, -) { - DisposableEffect(state) { - dataSource.subscribe(state) - onDispose { - dataSource.unsubscribe(state) - } +) = DisposableEffect(state) { + dataSource.subscribe(state) + onDispose { + dataSource.unsubscribe(state) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt index 92784e27a..1dade4aca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/ListWithUniqueSetCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/ListWithUniqueSetCache.kt index 2c599e226..d3a529294 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/ListWithUniqueSetCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/ListWithUniqueSetCache.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.authCommand.model -import androidx.lifecycle.AtomicReference +import java.util.concurrent.atomic.AtomicReference class ListWithUniqueSetCache( val key: (T) -> U, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/QueryBasedSubscriptionOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/QueryBasedSubscriptionOrchestrator.kt index e7d90743a..d06b01261 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/QueryBasedSubscriptionOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/QueryBasedSubscriptionOrchestrator.kt @@ -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( } 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( if (queries.isEmpty()) { stop() } - - if (isDebug) { - Log.d(this::class.simpleName, "Unwatch $query (${queries.size} queries)") - } } fun forEachSubscriber(action: (T) -> Unit) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt new file mode 100644 index 000000000..ea1c1df52 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt @@ -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 { + // 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 { + // 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)) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssembler.kt index 680f1141a..c61389b95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssembler.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index 27325b39d..1ed54ba0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -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 { ChannelFinderFilterAssemblerSubscription(baseChannel) - return baseChannel.live.observeAsState() + return baseChannel.flow.stateFlow.collectAsStateWithLifecycle() +} + +@Composable +fun observeChannelPicture(baseChannel: Channel): State { + // 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 { + // 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) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt index a8cf30741..bf5d8bf34 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt index 400e488bb..fa7a11cec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt @@ -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 { +fun observeNote(note: Note): State { // 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 observeNoteEvent(note: Note): State { 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 observeNoteAndMap( // Subscribe in the relay for changes in this note. EventFinderFilterAssemblerSubscription(note) + val flow = + remember(note) { + note + .flow() + .metadata.stateFlow + .mapLatest { map(it.note) } + .distinctUntilChanged() + .flowOn(Dispatchers.Default) + } + // Subscribe in the LocalCache for changes that arrive in the device - return note - .live() - .metadata - .map { map(it.note) } - .distinctUntilChanged() - .observeAsState(map(note)) + return flow.collectAsStateWithLifecycle(map(note)) } @Composable @@ -79,14 +96,18 @@ fun 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) } } - .distinctUntilChanged() - .observeAsState( - (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() + .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 { EventFinderFilterAssemblerSubscription(note) // Subscribe in the LocalCache for changes that arrive in the device - return note - .live() - .metadata - .map { it.note.event != null } - .distinctUntilChanged() - .observeAsState(note.event != null) + val flow = + remember(note) { + note + .flow() + .metadata.stateFlow + .mapLatest { it.note.event != null } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle(note.event != null) } @Composable @@ -109,7 +134,10 @@ fun observeNoteReplies(note: Note): State { 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 { EventFinderFilterAssemblerSubscription(note) // Subscribe in the LocalCache for changes that arrive in the device - return note - .live() - .replies - .map { it.note.reactions.size } - .distinctUntilChanged() - .observeAsState(note.reactions.size) + val flow = + remember(note) { + note + .flow() + .reactions.stateFlow + .mapLatest { it.note.replies.size } + .sample(1000) + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle(note.replies.size) } @Composable @@ -132,7 +165,10 @@ fun observeNoteReactions(note: Note): State { 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 { 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 { 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 { 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) } - .distinctUntilChanged() - .observeAsState(note.isBoostedBy(user)) + val flow = + remember(note) { + note + .flow() + .boosts.stateFlow + .mapLatest { it.note.isBoostedBy(user) } + .distinctUntilChanged() + .flowOn(Dispatchers.Default) + } + + return flow.collectAsStateWithLifecycle(note.isBoostedBy(user)) } @Composable @@ -193,12 +244,17 @@ fun observeNoteRepostCount(note: Note): State { EventFinderFilterAssemblerSubscription(note) // Subscribe in the LocalCache for changes that arrive in the device - return note - .live() - .boosts - .map { it.note.boosts.size } - .distinctUntilChanged() - .observeAsState(note.boosts.size) + val flow = + remember(note) { + note + .flow() + .boosts.stateFlow + .sample(1000) + .mapLatest { note.boosts.size } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle(note.boosts.size) } @Composable @@ -207,17 +263,18 @@ fun observeNoteReferences(note: Note): State { 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 - }.distinctUntilChanged() - .observeAsState( - note.zaps.isNotEmpty() || note.boosts.isNotEmpty() || note.reactions.isNotEmpty(), - ) + val flow = + remember(note) { + combine( + note.flow().zaps.stateFlow, + note.flow().boosts.stateFlow, + note.flow().reactions.stateFlow, + ) { zapState, boostState, reactionState -> + zapState.note.hasZapsBoostsOrReactions() + }.distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle(note.hasZapsBoostsOrReactions()) } @Composable @@ -226,7 +283,11 @@ fun observeNoteOts(note: Note): State { 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 { EventFinderFilterAssemblerSubscription(note) // Subscribe in the LocalCache for changes that arrive in the device - return note.live().edits.observeAsState() + return note + .flow() + .edits + .stateFlow + .collectAsStateWithLifecycle() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 75d9ec3e8..adea0737a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -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 { @@ -35,7 +49,10 @@ fun observeUser(user: User): State { 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 { // Subscribe in the relay for changes in the metadata of this user. UserFinderFilterAssemblerSubscription(user) + val flow = + remember(user) { + user + .flow() + .metadata.stateFlow + .mapLatest { it.user.toBestDisplayName() } + .distinctUntilChanged() + } + // Subscribe in the LocalCache for changes that arrive in the device - return user - .live() - .metadata - .map { it.user.toBestDisplayName() } - .distinctUntilChanged() - .observeAsState(user.toBestDisplayName()) + return flow.collectAsStateWithLifecycle(user.toBestDisplayName()) } @Composable @@ -57,13 +78,17 @@ fun observeUserNip05(user: User): State { // Subscribe in the relay for changes in the metadata of this user. UserFinderFilterAssemblerSubscription(user) + val flow = + remember(user) { + user + .flow() + .metadata.stateFlow + .mapLatest { it.user.info?.nip05 } + .distinctUntilChanged() + } + // Subscribe in the LocalCache for changes that arrive in the device - return user - .live() - .metadata - .map { it.user.info?.nip05 } - .distinctUntilChanged() - .observeAsState(user.info?.nip05) + return flow.collectAsStateWithLifecycle(user.info?.nip05) } @Composable @@ -72,12 +97,16 @@ fun observeUserAboutMe(user: User): State { UserFinderFilterAssemblerSubscription(user) // Subscribe in the LocalCache for changes that arrive in the device - return user - .live() - .metadata - .map { it.user.info?.about ?: "" } - .distinctUntilChanged() - .observeAsState(user.info?.about ?: "") + val flow = + remember(user) { + user + .flow() + .metadata.stateFlow + .mapLatest { it.user.info?.about ?: "" } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle(user.info?.about ?: "") } @Composable @@ -86,26 +115,34 @@ fun observeUserInfo(user: User): State { UserFinderFilterAssemblerSubscription(user) // Subscribe in the LocalCache for changes that arrive in the device - return user - .live() - .metadata - .map { it.user.info } - .distinctUntilChanged() - .observeAsState(user.info) + val flow = + remember(user) { + user + .flow() + .metadata.stateFlow + .mapLatest { it.user.info } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle(user.info) } @Composable -fun observeUserBanner(user: User): State { +fun observeUserBanner(user: User): State { // 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 ?: "" } - .distinctUntilChanged() - .observeAsState(user.info?.banner ?: "") + val flow = + remember(user) { + user + .flow() + .metadata.stateFlow + .mapLatest { it.user.info?.banner } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle(user.info?.banner) } @Composable @@ -114,12 +151,16 @@ fun observeUserPicture(user: User): State { UserFinderFilterAssemblerSubscription(user) // Subscribe in the LocalCache for changes that arrive in the device - return user - .live() - .metadata - .map { it.user.info?.picture } - .distinctUntilChanged() - .observeAsState(user.info?.picture) + val flow = + remember(user) { + user + .flow() + .metadata.stateFlow + .mapLatest { it.user.info?.picture } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle(user.info?.picture) } @Composable @@ -128,10 +169,400 @@ fun observeUserShortName(user: User): State { UserFinderFilterAssemblerSubscription(user) // Subscribe in the LocalCache for changes that arrive in the device - return user - .live() - .metadata - .map { it.user.toBestShortFirstName() } - .distinctUntilChanged() - .observeAsState(user.toBestShortFirstName()) + val flow = + remember(user) { + user + .flow() + .metadata.stateFlow + .mapLatest { it.user.toBestShortFirstName() } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle(user.toBestShortFirstName()) +} + +@Composable +fun observeUserFollows(user: User): State { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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> { + // 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 { + // 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 { + // 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 = emptyList(), + val userRelayList: List = emptyList(), +) + +@Composable +fun observeUserRelaysUsing(user: User): State { + // 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()) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/MutableQueryBasedSubscriptionOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/MutableQueryBasedSubscriptionOrchestrator.kt index 5db17d5da..d87e36811 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/MutableQueryBasedSubscriptionOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/MutableQueryBasedSubscriptionOrchestrator.kt @@ -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( } 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( if (queries.isEmpty()) { stop() } - - if (isDebug) { - Log.d(this::class.simpleName, "Unwatch $query (${queries.size} queries)") - } } fun forEachSubscriber(action: (T) -> Unit) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt index 696c270ad..3d59b4f2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt index 655f03876..56e9e8089 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index f0e7b2ecf..3892e3d91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt index e55a0057e..de4762fa2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index 717b031dd..f8914bb41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt index edf0dea2b..d342f786d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/util/DeviceUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/util/DeviceUtils.kt index 3e3f3d11c..f80807c93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/util/DeviceUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/util/DeviceUtils.kt @@ -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 { /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt index 6391031f9..b0c18026e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/DefaultFeedOrder.kt @@ -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 = - compareByDescending - { - val noteEvent = it.event - if (noteEvent == null) { - null - } else { - if (noteEvent is Event) { - noteEvent.createdAt - } else { - null - } - } - }.thenBy { it.idHex } + compareByDescending { it.event?.createdAt }.thenBy { it.idHex } val DefaultFeedOrderCard: Comparator = compareByDescending { it.createdAt() }.thenBy { it.id() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index cdba7b693..8bf7f3176 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 4c2408dde..740ff56fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt index 3d5409ee8..93601e166 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index 5673d420b..ce60f2f62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 09117af87..1a767fa53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -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) -> Unit, ) { - var statuses: ImmutableList 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index db1716ad0..7767525f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -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,23 +136,21 @@ 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( - it, - statuses, - baseUser, - columnModifier, - accountViewModel, - nav, - ) - } + CrossfadeIfEnabled( + targetState = nip05, + modifier = columnModifier, + accountViewModel = accountViewModel, + ) { + VerifyAndDisplayNIP05OrStatusLine( + it, + statuses, + baseUser, + columnModifier, + accountViewModel, + nav, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index d60939683..08567dade 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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,24 +1190,16 @@ 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, - contentDescription = stringRes(R.string.group_picture), - modifier = MaterialTheme.colorScheme.channelNotePictureModifier, - loadProfilePicture = loadProfilePicture, - loadRobohash = loadRobohash, - ) - } + RobohashFallbackAsyncImage( + robot = baseChannel.idHex, + model = model, + contentDescription = stringRes(R.string.group_picture), + modifier = MaterialTheme.colorScheme.channelNotePictureModifier, + loadProfilePicture = loadProfilePicture, + loadRobohash = loadRobohash, + ) } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 96ad324ec..dc9692760 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -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 LiveData.combineWith( - liveData1: LiveData, - block: (T?, K?) -> R, -): LiveData { - val result = MediatorLiveData() - result.addSource(this) { result.value = block(this.value, liveData1.value) } - result.addSource(liveData1) { result.value = block(this.value, liveData1.value) } - return result -} - -fun LiveData.combineWith( - liveData1: LiveData, - liveData2: LiveData

, - block: (T?, K?, P?) -> R, -): LiveData { - val result = MediatorLiveData() - 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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 9fcd40a79..17a05a7be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -223,11 +223,9 @@ class UpdateZapAmountViewModel : ViewModel() { fun updateNIP47(uri: String) { val contact = Nip47WalletConnect.parse(uri) - if (contact != null) { - walletConnectPubkey = TextFieldValue(contact.pubKeyHex) - walletConnectRelay = TextFieldValue(contact.relayUri ?: "") - walletConnectSecret = TextFieldValue(contact.secret ?: "") - } + walletConnectPubkey = TextFieldValue(contact.pubKeyHex) + walletConnectRelay = TextFieldValue(contact.relayUri) + walletConnectSecret = TextFieldValue(contact.secret ?: "") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index ab2eda8e8..59c2e7ad7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -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,12 +161,11 @@ fun UserActionOptions( baseAuthor: User, accountViewModel: AccountViewModel, ) { - WatchIsHiddenUser(baseAuthor, accountViewModel) { isHidden -> - if (isHidden) { - ShowUserButton { accountViewModel.show(baseAuthor) } - } else { - ShowFollowingOrUnfollowingButton(baseAuthor, accountViewModel) - } + val isHidden by observeAccountIsHiddenUser(accountViewModel.account, baseAuthor) + if (isHidden) { + ShowUserButton { accountViewModel.show(baseAuthor) } + } else { + ShowFollowingOrUnfollowingButton(baseAuthor, accountViewModel) } } @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt index d6fc2f8a2..4964cedba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt index d28f44b6f..18414b0af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapraiser/AddZapraiserButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapraiser/AddZapraiserButton.kt index 13f7376e0..f45d8d4d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapraiser/AddZapraiserButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapraiser/AddZapraiserButton.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitBuilder.kt index 1fd52fc0e..562f4b5a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitBuilder.kt @@ -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 { var items: List> by mutableStateOf(emptyList()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt index 8365ca5f3..20e46214a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt @@ -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, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt index a686b891c..6d686623b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index 3ed6aba7a..271c3f780 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index aa4e9994c..3384d14e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index a4dc22f31..0a302904a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -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 } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index 1e52c2f01..8371e239e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -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 { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 17a6a217d..e24889c57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt index 7b1562f6f..a8536dbaf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt @@ -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() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt index ff01baed5..99f3867ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt @@ -60,24 +60,20 @@ class ChatroomFilterAssembler( ) fun createMessagesFromMeFilter(key: ChatroomQueryState): TypedFilter? = - if (key.room != null) { - TypedFilter( - types = setOf(FeedType.PRIVATE_DMS), - filter = - SincePerRelayFilter( - kinds = listOf(PrivateDmEvent.KIND), - authors = listOf(key.account.userProfile().pubkeyHex), - tags = mapOf("p" to key.room.users.map { it }), - since = - latestEOSEs.users[key.account.userProfile()] - ?.followList - ?.get(key.room.hashCode().toString()) - ?.relayList, - ), - ) - } else { - null - } + TypedFilter( + types = setOf(FeedType.PRIVATE_DMS), + filter = + SincePerRelayFilter( + kinds = listOf(PrivateDmEvent.KIND), + authors = listOf(key.account.userProfile().pubkeyHex), + tags = mapOf("p" to key.room.users.map { it }), + since = + latestEOSEs.users[key.account.userProfile()] + ?.followList + ?.get(key.room.hashCode().toString()) + ?.relayList, + ), + ) fun clearEOSEs(account: Account) { latestEOSEs.removeDataFor(account.userProfile()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt index 4d309df3a..ea8f82b1f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 529649d36..ac74215bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index bdfc31231..3316c00be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt index 8d804817f..81d56d8e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -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) + 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() } + } + if (isMe) { EditButton(channel, accountViewModel, nav) } - - WatchChannelFollows(channel, accountViewModel) { isFollowing -> - if (isFollowing) { - LeaveChatButton(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) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt index 2f034d451..a4a42d252 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt index a87cda003..f639832b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt @@ -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 -> - if (!isFollowing) { - JoinChatButton(channel, accountViewModel, nav) - } + 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) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt index 5f1a1d652..a142c910f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt index 01cf7c51c..db0ff5803 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt index a86daef86..3f37ed6da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 6868bca4a..3bf719d6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -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() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt index 36254a842..a38dabe5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt @@ -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) - - 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(), - artworkUri = event.image(), - authorName = baseChannel.creatorName(), - uri = baseChannel.toNAddr(), - ) - } - - ZoomableContentView( - content = zoomableUrlVideo, - roundedCorner = false, - contentScale = ContentScale.FillWidth, - accountViewModel = accountViewModel, - ) - } + val streamingInfoEvent by observeChannelInfo(baseChannel) + streamingInfoEvent?.let { event -> + event.streaming()?.let { url -> + val zoomableUrlVideo = + remember(streamingInfoEvent) { + MediaUrlVideo( + url = url, + 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 = media, + roundedCorner = false, + contentScale = ContentScale.FillWidth, + accountViewModel = accountViewModel, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt index 45135dbac..f1a6c47b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index df7eb0473..444dca63e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 6656e4f32..bf8d29ba3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt index 62ba9da49..6d28b8c75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt index 8b9ae66fe..8b3748699 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt index 9efd062cd..d8972b9e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt index 23ca40de0..d6fb4eeba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt index 85cd3de8d..d5ed08dd2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -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 { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt index 28506b8f7..fe21ee4eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt @@ -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 { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt index dcbc827bb..577a3bc43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt index 821504ae3..7e0961e5e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index fd914f7c0..eab695472 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -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() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt index bbb4f51e9..c53291336 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt @@ -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)}") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt index cefee9954..1c1aff8da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt index 082576223..fcd9a42cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt @@ -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) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt index 2d9425789..91371741d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt @@ -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() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt index c1c324ffa..2216db55f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt @@ -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) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt index 77a059efe..23c0e9e73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt @@ -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() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt index a7d6c7c72..e4ce1e735 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt index 301a5262e..f14ca7f9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt @@ -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)}") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt index 6a474f8f3..01caf423b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt @@ -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 { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt index f74bb6eaa..05331db62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt @@ -53,10 +53,19 @@ fun DrawBanner( ) { val banner by observeUserBanner(baseUser) - val clipboardManager = LocalClipboardManager.current - var zoomImageDialogOpen by remember { mutableStateOf(false) } + 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), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt index 51f0ea020..d2ef86a5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt @@ -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 -> - if (isHidden) { - ShowUserButton { accountViewModel.showUser(baseUser.pubkeyHex) } - } else { - DisplayFollowUnfollowButton(baseUser, accountViewModel) - } + val isHidden by observeAccountIsHiddenUser(accountViewModel.account, baseUser) + + if (isHidden) { + ShowUserButton { accountViewModel.showUser(baseUser.pubkeyHex) } + } else { + DisplayFollowUnfollowButton(baseUser, accountViewModel) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index a40162b8d..84422fd34 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -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 = 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) - - _feedContent.update { newList } + 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, + relays: Map?, + ): List { + 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() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt index 5b000bc80..3b895757a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt @@ -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 "--" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt index 35a41495e..80caa6068 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt @@ -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)}") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt index 137f8d934..04a5b818b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt @@ -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() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt index 7fffd030a..df446dbc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchIsHiddenUser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchIsHiddenUser.kt deleted file mode 100644 index c66cf9767..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchIsHiddenUser.kt +++ /dev/null @@ -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) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt index 38f54d328..823ee4116 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt @@ -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() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt index 1a8eb58ef..bab1d5c32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt @@ -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(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)}") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt index 6b561f488..3a072e134 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt @@ -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() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index cdec89168..751161ed4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -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 { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt index 8a35dfefe..4adb4ebf5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt index 0a8b0d02c..b164495a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt @@ -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() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt index 31773c6cb..d13e0525c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt @@ -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. diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 6a34c855e..2c95f4e76 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -77,6 +77,9 @@ Error parsing error message " Following" " Followers" + "%1$s Following" + "%1$s Followers" + Profile Security Filters Logout @@ -163,6 +166,7 @@ Gallery "Follows" "Reports" + "%1$s Reports" More Options " Relays" Website diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 2f52b4cf5..6e977718e 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -354,9 +354,6 @@ fun TranslateAndWatchLanguageChanges( accountViewModel: AccountViewModel, onTranslated: (TranslationConfig) -> Unit, ) { - // Don't automatically update translations. - // val accountState by accountViewModel.accountLanguagesLiveData.observeAsState() - LaunchedEffect(Unit) { // This takes some time. Launches as a Composition scope to make sure this gets cancel if this // item gets out of view. diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt index d859c78ca..589e0d64a 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt @@ -278,38 +278,34 @@ object MetaTagsParser { State.VALUE -> { var attr: Pair? = null - when { - valueQuote != null -> { - if (c == valueQuote) { + if (valueQuote != null) { + if (c == valueQuote) { + attr = + Pair( + input.slice(nameBegin.. { attr = Pair( input.slice(nameBegin.. { - when { - c.isWhitespace() -> { - attr = - Pair( - input.slice(nameBegin.. { + attr = + Pair( + input.slice(nameBegin.. { - attr = - Pair( - input.slice(nameBegin.. { - return null - } + NON_UNQUOTED_ATTR_VALUE_CHARS.contains(c) -> { + return null } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1fdf46e5..73c93e672 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,7 +74,6 @@ androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", ve androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" } androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxJunit" } -androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } @@ -88,7 +87,6 @@ androidx-media3-session = { group = "androidx.media3", name = "media3-session", androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" } -androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } androidx-runtime-runtime = { group = "androidx.compose.runtime", name = "runtime" } androidx-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "securityCryptoKtx" } androidx-ui = { group = "androidx.compose.ui", name = "ui" }