diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index b6bc60fd9..c3a4448d1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -7,14 +7,11 @@ import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.service.NostrHomeDataSource -import com.vitorpamplona.amethyst.service.NostrNotificationDataSource import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource import com.vitorpamplona.amethyst.service.relays.Client object ServiceManager { @@ -34,25 +31,18 @@ object ServiceManager { // start services NostrAccountDataSource.account = myAccount NostrHomeDataSource.account = myAccount - NostrNotificationDataSource.account = myAccount NostrChatroomListDataSource.account = myAccount - NostrGlobalDataSource.account = myAccount - NostrChannelDataSource.account = myAccount - - NostrUserProfileDataSource.account = myAccount - NostrUserProfileFollowsDataSource.account = myAccount - NostrUserProfileFollowersDataSource.account = myAccount + // Notification Elements NostrAccountDataSource.start() - //NostrGlobalDataSource.start() NostrHomeDataSource.start() - NostrNotificationDataSource.start() + NostrChatroomListDataSource.start() + + // More Info Data Sources NostrSingleEventDataSource.start() NostrSingleChannelDataSource.start() NostrSingleUserDataSource.start() - //NostrThreadDataSource.start() - NostrChatroomListDataSource.start() } else { // if not logged in yet, start a basic service wit default relays Client.connect(Constants.convertDefaultRelays()) @@ -65,11 +55,8 @@ object ServiceManager { NostrChannelDataSource.stop() NostrChatroomListDataSource.stop() NostrUserProfileDataSource.stop() - NostrUserProfileFollowersDataSource.stop() - NostrUserProfileFollowsDataSource.stop() NostrGlobalDataSource.stop() - NostrNotificationDataSource.stop() NostrSingleChannelDataSource.stop() NostrSingleEventDataSource.stop() NostrSingleUserDataSource.stop() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index d0df9852c..fe369239e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1,10 +1,8 @@ package com.vitorpamplona.amethyst.model import android.content.res.Resources -import android.util.Log import androidx.core.os.ConfigurationCompat import androidx.lifecycle.LiveData -import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent @@ -406,11 +404,9 @@ class Account( } init { - GlobalScope.launch(Dispatchers.Main) { - userProfile().liveRelays.observeForever { - GlobalScope.launch(Dispatchers.IO) { - reconnectIfRelaysHaveChanged() - } + userProfile().liveRelays.observeForever { + GlobalScope.launch(Dispatchers.IO) { + reconnectIfRelaysHaveChanged() } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 2d316c99b..11e5d89f1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.model import androidx.lifecycle.LiveData +import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.ui.note.toShortenHex @@ -60,12 +61,12 @@ class ChannelLiveData(val channel: Channel): LiveData(ChannelState override fun onActive() { super.onActive() - NostrSingleEventDataSource.add(channel.idHex) + NostrSingleChannelDataSource.add(channel.idHex) } override fun onInactive() { super.onInactive() - NostrSingleEventDataSource.remove(channel.idHex) + NostrSingleChannelDataSource.remove(channel.idHex) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index a706ac80e..df8eea2cb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -47,7 +47,6 @@ object LocalCache { val users = ConcurrentHashMap() val notes = ConcurrentHashMap() val channels = ConcurrentHashMap() - @Synchronized fun getOrCreateUser(key: HexKey): User { return users[key] ?: run { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt new file mode 100644 index 000000000..b718ffa52 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -0,0 +1,68 @@ +package com.vitorpamplona.amethyst.model + +import kotlin.time.ExperimentalTime +import kotlin.time.measureTimedValue + +class ThreadAssembler { + + fun searchRoot(note: Note, testedNotes: MutableSet = mutableSetOf()): Note? { + if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note + + val markedAsRoot = note.event?.tags?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) + if (markedAsRoot != null) return LocalCache.getOrCreateNote(markedAsRoot) + + val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true } + if (hasNoReplyTo != null) return hasNoReplyTo + + testedNotes.add(note) + + // recursive + val roots = note.replyTo?.map { + if (it !in testedNotes) + searchRoot(it, testedNotes) + else + null + }?.filterNotNull() + + if (roots != null && roots.isNotEmpty()) { + return roots[0] + } + + return null + } + + @OptIn(ExperimentalTime::class) + fun findThreadFor(noteId: String): Set { + val (result, elapsed) = measureTimedValue { + val note = LocalCache.getOrCreateNote(noteId) + + if (note.event != null) { + val thread = mutableListOf() + val threadSet = mutableSetOf() + + val threadRoot = searchRoot(note) ?: note + + loadDown(threadRoot, thread, threadSet) + + thread.toSet() + } else { + setOf(note) + } + } + + println("Model Refresh: Thread loaded in ${elapsed}") + + return result + } + + fun loadDown(note: Note, thread: MutableList, threadSet: MutableSet) { + if (note !in threadSet) { + thread.add(note) + threadSet.add(note) + + note.replies.forEach { + loadDown(it, thread, threadSet) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index bd1d28160..e53003735 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -15,7 +15,7 @@ import nostr.postr.events.ContactListEvent import nostr.postr.events.MetadataEvent import nostr.postr.events.TextNoteEvent -object NostrAccountDataSource: NostrDataSource("AccountData") { +object NostrAccountDataSource: NostrDataSource("AccountData") { lateinit var account: Account fun createAccountContactListFilter(): TypedFilter { @@ -63,15 +63,6 @@ object NostrAccountDataSource: NostrDataSource("AccountData") { val accountChannel = requestNewChannel() - override fun feed(): List { - val user = account.userProfile() - - return LocalCache.notes.values - .filter { (it.event is TextNoteEvent || it.event is RepostEvent) && it.author in user.follows } - .sortedBy { it.event?.createdAt } - .reversed() - } - override fun updateChannelFilters() { // gets everthing about the user logged in accountChannel.typedFilters = listOf( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt index 37d1a65be..85a894cf9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt @@ -1,6 +1,6 @@ package com.vitorpamplona.amethyst.service -import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent @@ -8,9 +8,8 @@ import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -object NostrChannelDataSource: NostrDataSource("ChatroomFeed") { - lateinit var account: Account - var channel: com.vitorpamplona.amethyst.model.Channel? = null +object NostrChannelDataSource: NostrDataSource("ChatroomFeed") { + var channel: Channel? = null fun loadMessagesBetween(channelId: String) { channel = LocalCache.getOrCreateChannel(channelId) @@ -33,11 +32,6 @@ object NostrChannelDataSource: NostrDataSource("ChatroomFeed") { val messagesChannel = requestNewChannel() - // returns the last Note of each user. - override fun feed(): List { - return channel?.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList() - } - override fun updateChannelFilters() { messagesChannel.typedFilters = listOfNotNull(createMessagesToChannelFilter()).ifEmpty { null } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt similarity index 80% rename from app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt rename to app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt index 5c27273dc..24d53d1a8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt @@ -9,13 +9,14 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter import nostr.postr.events.PrivateDmEvent -object NostrChatRoomDataSource: NostrDataSource("ChatroomFeed") { +object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") { lateinit var account: Account var withUser: User? = null fun loadMessagesBetween(accountIn: Account, userId: String) { account = accountIn withUser = LocalCache.users[userId] + resetFilters() } fun createMessagesToMeFilter(): TypedFilter? { @@ -40,7 +41,7 @@ object NostrChatRoomDataSource: NostrDataSource("ChatroomFeed") { return if (myPeer != null) { TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), + types = setOf(FeedType.PRIVATE_DMS), filter = JsonFilter( kinds = listOf(PrivateDmEvent.kind), authors = listOf(account.userProfile().pubkeyHex), @@ -54,13 +55,6 @@ object NostrChatRoomDataSource: NostrDataSource("ChatroomFeed") { val inandoutChannel = requestNewChannel() - // returns the last Note of each user. - override fun feed(): List { - val messages = account.userProfile().privateChatrooms[withUser] ?: return emptyList() - - return messages.roomMessages.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed() - } - override fun updateChannelFilters() { inandoutChannel.typedFilters = listOfNotNull(createMessagesToMeFilter(), createMessagesFromMeFilter()).ifEmpty { null } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index be7f400ee..ead2962fe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -10,7 +10,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter import nostr.postr.events.PrivateDmEvent -object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { +object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { lateinit var account: Account fun createMessagesToMeFilter() = TypedFilter( @@ -73,22 +73,6 @@ object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { val chatroomListChannel = requestNewChannel() - // returns the last Note of each user. - override fun feed(): List { - val privateChatrooms = account.userProfile().privateChatrooms - val messagingWith = privateChatrooms.keys.filter { account.isAcceptable(it) } - - val privateMessages = messagingWith.mapNotNull { - privateChatrooms[it]?.roomMessages?.sortedBy { it.event?.createdAt }?.lastOrNull { it.event != null } - } - - val publicChannels = account.followingChannels().map { - it.notes.values.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null } - } - - return (privateMessages + publicChannels).filterNotNull().sortedBy { it.event?.createdAt }.reversed() - } - override fun updateChannelFilters() { val list = listOf( createMessagesToMeFilter(), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 2fc27f7c4..ca61a47eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -33,9 +33,8 @@ import nostr.postr.events.PrivateDmEvent import nostr.postr.events.RecommendRelayEvent import nostr.postr.events.TextNoteEvent -abstract class NostrDataSource(val debugName: String) { +abstract class NostrDataSource(val debugName: String) { private var subscriptions = mapOf() - data class Counter(var counter:Int) private var eventCounter = mapOf() @@ -139,26 +138,6 @@ abstract class NostrDataSource(val debugName: String) { } } - fun loadTop(): List { - val returningList = feed().take(1000) - - // prepare previews - val scope = CoroutineScope(Job() + Dispatchers.IO) - scope.launch { - loadPreviews(returningList) - } - - return returningList - } - - fun loadPreviews(list: List) { - list.forEach { - if (it is Note) { - UrlCachedPreviewer.preloadPreviewsFor(it) - } - } - } - fun requestNewChannel(onEOSE: ((Long) -> Unit)? = null): Subscription { val newSubscription = Subscription(UUID.randomUUID().toString().substring(0,4), onEOSE) subscriptions = subscriptions + Pair(newSubscription.id, newSubscription) @@ -231,5 +210,4 @@ abstract class NostrDataSource(val debugName: String) { } abstract fun updateChannelFilters() - abstract fun feed(): List } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt index e56ad00c3..e68fd5653 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt @@ -9,7 +9,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter import nostr.postr.events.TextNoteEvent -object NostrGlobalDataSource: NostrDataSource("GlobalFeed") { +object NostrGlobalDataSource: NostrDataSource("GlobalFeed") { lateinit var account: Account fun createGlobalFilter() = TypedFilter( types = setOf(FeedType.GLOBAL), @@ -21,15 +21,6 @@ object NostrGlobalDataSource: NostrDataSource("GlobalFeed") { val globalFeedChannel = requestNewChannel() - override fun feed() = LocalCache.notes.values - .filter { account.isAcceptable(it) } - .filter { - (it.event is TextNoteEvent && (it.event as TextNoteEvent).replyTos.isEmpty()) || - (it.event is ChannelMessageEvent && (it.event as ChannelMessageEvent).replyTos.isEmpty()) - } - .sortedBy { it.event?.createdAt } - .reversed() - override fun updateChannelFilters() { globalFeedChannel.typedFilters = listOf(createGlobalFilter()).ifEmpty { null } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index 6daad5fc3..0ef892de8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -16,7 +16,7 @@ import nostr.postr.JsonFilter import nostr.postr.events.TextNoteEvent import nostr.postr.toHex -object NostrHomeDataSource: NostrDataSource("HomeFeed") { +object NostrHomeDataSource: NostrDataSource("HomeFeed") { lateinit var account: Account private val cacheListener: (UserState) -> Unit = { @@ -62,16 +62,6 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") { val followAccountChannel = requestNewChannel() - override fun feed(): List { - val user = account.userProfile() - - return LocalCache.notes.values - .filter { (it.event is TextNoteEvent || it.event is RepostEvent) && it.author in user.follows } - .filter { account.isAcceptable(it) } - .sortedBy { it.event?.createdAt } - .reversed() - } - override fun updateChannelFilters() { followAccountChannel.typedFilters = listOf(createFollowAccountsFilter()).ifEmpty { null } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index 8bf119a38..229c68bc5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -11,7 +11,7 @@ import nostr.postr.events.MetadataEvent import nostr.postr.events.TextNoteEvent import nostr.postr.toHex -object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") { +object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") { private var hexToWatch: String? = null private fun createAnythingWithIDFilter(): List? { @@ -39,10 +39,6 @@ object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed" val searchChannel = requestNewChannel() - override fun feed(): List { - return emptyList() - } - override fun updateChannelFilters() { searchChannel.typedFilters = createAnythingWithIDFilter() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt index e96c8eb4d..f9b211c51 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt @@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -object NostrSingleChannelDataSource: NostrDataSource("SingleChannelFeed") { +object NostrSingleChannelDataSource: NostrDataSource("SingleChannelFeed") { private var channelsToWatch = setOf() private fun createRepliesAndReactionsFilter(): TypedFilter? { @@ -51,10 +51,6 @@ object NostrSingleChannelDataSource: NostrDataSource("SingleChannelFeed") val singleChannelChannel = requestNewChannel() - override fun feed(): List { - return emptyList() - } - override fun updateChannelFilters() { val reactions = createRepliesAndReactionsFilter() val missing = createLoadEventsIfNotLoadedFilter() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 5e3386aae..422d2b219 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -15,7 +15,7 @@ import java.util.Date import nostr.postr.JsonFilter import nostr.postr.events.TextNoteEvent -object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { +object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { private var eventsToWatch = setOf() private fun createRepliesAndReactionsFilter(): List? { @@ -87,14 +87,6 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { invalidateFilters() } - override fun feed(): List { - return synchronized(eventsToWatch) { - eventsToWatch.map { - LocalCache.notes[it] - }.filterNotNull() - } - } - override fun updateChannelFilters() { val reactions = createRepliesAndReactionsFilter() val missing = createLoadEventsIfNotLoadedFilter() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index 3a8b74d95..23f408ea3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter import nostr.postr.events.MetadataEvent -object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { +object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { var usersToWatch = setOf() fun createUserFilter(): List? { @@ -46,14 +46,6 @@ object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { invalidateFilters() } - override fun feed(): List { - return synchronized(usersToWatch) { - usersToWatch.map { - LocalCache.users[it] - }.filterNotNull() - } - } - override fun updateChannelFilters() { userChannel.typedFilters = listOfNotNull(createUserFilter(), createUserReportFilter()).flatten() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt index f5b8e50c8..71cf993d0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt @@ -2,117 +2,48 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter +import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter import nostr.postr.JsonFilter import nostr.postr.events.TextNoteEvent -object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") { - private var eventsToWatch = setOf() - - fun createRepliesAndReactionsFilter(): TypedFilter? { - if (eventsToWatch.isEmpty()) { - return null - } - - return TypedFilter( - types = FeedType.values().toSet(), - filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind), - tags = mapOf("e" to eventsToWatch.toList()) - ) - ) - } +object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") { + private var eventToWatch: String? = null fun createLoadEventsIfNotLoadedFilter(): TypedFilter? { - val nodes = eventsToWatch.map { LocalCache.getOrCreateNote(it) } + val threadToLoad = eventToWatch ?: return null - val eventsToLoad = nodes + val eventsToLoad = ThreadAssembler().findThreadFor(threadToLoad) .filter { it.event == null } - .map { it.idHex.substring(0, 8) } - - if (eventsToLoad.isEmpty()) { - return null - } + .map { it.idHex } + .toSet() + .ifEmpty { null } ?: return null return TypedFilter( types = FeedType.values().toSet(), filter = JsonFilter( - ids = eventsToLoad + ids = eventsToLoad.map { it.substring(0, 8) } ) ) } - val loadEventsChannel = requestNewChannel() - - override fun feed(): List { - // Currently orders by date of each event, descending, at each level of the reply stack - val order = compareByDescending { it.replyLevelSignature() } - - return eventsToWatch.map { - LocalCache.getOrCreateNote(it) - }.sortedWith(order) - } - - override fun updateChannelFilters() { - loadEventsChannel.typedFilters = listOfNotNull(createLoadEventsIfNotLoadedFilter(), createRepliesAndReactionsFilter()).ifEmpty { null } - } - - fun searchRoot(note: Note, testedNotes: MutableSet = mutableSetOf()): Note? { - if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note - - val markedAsRoot = note.event?.tags?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) - if (markedAsRoot != null) return LocalCache.getOrCreateNote(markedAsRoot) - - val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true } - if (hasNoReplyTo != null) return hasNoReplyTo - - testedNotes.add(note) - - // recursive - val roots = note.replyTo?.map { - if (it !in testedNotes) - searchRoot(it, testedNotes) - else - null - }?.filterNotNull() - - if (roots != null && roots.isNotEmpty()) { - return roots[0] - } - - return null - } - - fun loadThread(noteId: String) { - val note = LocalCache.getOrCreateNote(noteId) - - if (note.event != null) { - val thread = mutableListOf() - val threadSet = mutableSetOf() - - val threadRoot = searchRoot(note) ?: note - - loadDown(threadRoot, thread, threadSet) - - eventsToWatch = thread.map { it.idHex }.toSet() - } else { - eventsToWatch = setOf(noteId) - } - + val loadEventsChannel = requestNewChannel(){ + // Many relays operate with limits in the amount of filters. + // As information comes, the filters will be rotated to get more data. invalidateFilters() } - fun loadDown(note: Note, thread: MutableList, threadSet: MutableSet) { - if (note !in threadSet) { - thread.add(note) - threadSet.add(note) + override fun updateChannelFilters() { + loadEventsChannel.typedFilters = listOfNotNull(createLoadEventsIfNotLoadedFilter()).ifEmpty { null } + } - note.replies.forEach { - loadDown(it, thread, threadSet) - } - } + fun loadThread(noteId: String?) { + eventToWatch = noteId + + invalidateFilters() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 0ef6f25fb..8a1154232 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.service -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -12,12 +11,15 @@ import nostr.postr.events.ContactListEvent import nostr.postr.events.MetadataEvent import nostr.postr.events.TextNoteEvent -object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { - lateinit var account: Account +object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { var user: User? = null - fun loadUserProfile(userId: String) { - user = LocalCache.getOrCreateUser(userId) + fun loadUserProfile(userId: String?) { + if (userId != null) { + user = LocalCache.getOrCreateUser(userId) + } + + resetFilters() } fun createUserInfoFilter(): TypedFilter { @@ -73,14 +75,6 @@ object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { val userInfoChannel = requestNewChannel() - override fun feed(): List { - return user?.notes - ?.filter { account.isAcceptable(it) } - ?.sortedBy { it.event?.createdAt } - ?.reversed() - ?: emptyList() - } - override fun updateChannelFilters() { userInfoChannel.typedFilters = listOf( createUserInfoFilter(), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 166878a1a..845d0f129 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -18,6 +18,7 @@ import coil.decode.SvgDecoder import coil.disk.DiskCache import coil.memory.MemoryCache import coil.request.CachePolicy +import coil.util.DebugLogger import com.vitorpamplona.amethyst.EncryptedStorage import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager @@ -51,7 +52,7 @@ class MainActivity : ComponentActivity() { add(GifDecoder.Factory()) } add(SvgDecoder.Factory()) - } + } //.logger(DebugLogger()) .respectCacheHeaders(false) .build() } @@ -76,7 +77,7 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() // Only starts after login - //ServiceManager.start() + ServiceManager.start() } override fun onPause() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt new file mode 100644 index 000000000..e4eb97e10 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt @@ -0,0 +1,25 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.relays.FeedType +import com.vitorpamplona.amethyst.service.relays.TypedFilter +import nostr.postr.JsonFilter + +object ChannelFeedFilter: FeedFilter() { + lateinit var account: Account + lateinit var channel: Channel + + fun loadMessagesBetween(accountLoggedIn: Account, channelId: String) { + account = accountLoggedIn + channel = LocalCache.getOrCreateChannel(channelId) + } + + // returns the last Note of each user. + override fun feed(): List { + return channel?.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt new file mode 100644 index 000000000..807537f8a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt @@ -0,0 +1,23 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User + +object ChatroomFeedFilter: FeedFilter() { + lateinit var account: Account + lateinit var withUser: User + + fun loadMessagesBetween(accountIn: Account, userId: String) { + account = accountIn + withUser = LocalCache.getOrCreateUser(userId) + } + + // returns the last Note of each user. + override fun feed(): List { + val messages = account.userProfile().privateChatrooms[withUser] ?: return emptyList() + + return messages.roomMessages.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt new file mode 100644 index 000000000..678eace2c --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt @@ -0,0 +1,33 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note + +object ChatroomListKnownFeedFilter: FeedFilter() { + lateinit var account: Account + + // returns the last Note of each user. + override fun feed(): List { + val me = account.userProfile() + + val privateChatrooms = account.userProfile().privateChatrooms + val messagingWith = privateChatrooms.keys.filter { + me.hasSentMessagesTo(it) && account.isAcceptable(it) + } + + val privateMessages = messagingWith.mapNotNull { + privateChatrooms[it]?.roomMessages?.sortedBy { + it.event?.createdAt + }?.lastOrNull { + it.event != null + } + } + + val publicChannels = account.followingChannels().map { + it.notes.values.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null } + } + + return (privateMessages + publicChannels).filterNotNull().sortedBy { it.event?.createdAt }.reversed() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt new file mode 100644 index 000000000..3bb7eb510 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt @@ -0,0 +1,29 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note + +object ChatroomListNewFeedFilter: FeedFilter() { + lateinit var account: Account + + // returns the last Note of each user. + override fun feed(): List { + val me = ChatroomListKnownFeedFilter.account.userProfile() + + val privateChatrooms = account.userProfile().privateChatrooms + val messagingWith = privateChatrooms.keys.filter { + !me.hasSentMessagesTo(it) && account.isAcceptable(it) + } + + val privateMessages = messagingWith.mapNotNull { + privateChatrooms[it]?.roomMessages?.sortedBy { + it.event?.createdAt + }?.lastOrNull { + it.event != null + } + } + + return privateMessages.sortedBy { it.event?.createdAt }.reversed() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt new file mode 100644 index 000000000..f98bbf263 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -0,0 +1,18 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.UrlCachedPreviewer +import kotlin.time.ExperimentalTime +import kotlin.time.measureTimedValue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +abstract class FeedFilter() { + fun loadTop(): List { + return feed().take(1000) + } + + abstract fun feed(): List +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt new file mode 100644 index 000000000..513a26a3a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt @@ -0,0 +1,24 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.relays.FeedType +import com.vitorpamplona.amethyst.service.relays.TypedFilter +import nostr.postr.JsonFilter +import nostr.postr.events.TextNoteEvent + +object GlobalFeedFilter: FeedFilter() { + lateinit var account: Account + + override fun feed() = LocalCache.notes.values + .filter { account.isAcceptable(it) } + .filter { + (it.event is TextNoteEvent && (it.event as TextNoteEvent).replyTos.isEmpty()) || + (it.event is ChannelMessageEvent && (it.event as ChannelMessageEvent).replyTos.isEmpty()) + } + .sortedBy { it.event?.createdAt } + .reversed() + +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHiddenAccountsDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt similarity index 67% rename from app/src/main/java/com/vitorpamplona/amethyst/service/NostrHiddenAccountsDataSource.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt index 28079d7e6..3e09008fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHiddenAccountsDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt @@ -1,4 +1,4 @@ -package com.vitorpamplona.amethyst.service +package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache @@ -7,10 +7,8 @@ import com.vitorpamplona.amethyst.model.User import nostr.postr.JsonFilter import nostr.postr.events.TextNoteEvent -object NostrHiddenAccountsDataSource: NostrDataSource("HiddenAccounts") { +object HiddenAccountsFeedFilter: FeedFilter() { lateinit var account: Account override fun feed() = account.hiddenUsers() - - override fun updateChannelFilters() {} } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt new file mode 100644 index 000000000..7f3451791 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -0,0 +1,25 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.RepostEvent +import nostr.postr.events.TextNoteEvent + +object HomeConversationsFeedFilter: FeedFilter() { + lateinit var account: Account + + override fun feed(): List { + val user = account.userProfile() + + return LocalCache.notes.values + .filter { + (it.event is TextNoteEvent || it.event is RepostEvent) + && it.author in user.follows + && account.isAcceptable(it) + && !it.isNewThread() + } + .sortedBy { it.event?.createdAt } + .reversed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt new file mode 100644 index 000000000..7912ba2f6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -0,0 +1,35 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.LocalCacheState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.UserState +import com.vitorpamplona.amethyst.service.model.RepostEvent +import com.vitorpamplona.amethyst.service.relays.FeedType +import com.vitorpamplona.amethyst.service.relays.TypedFilter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import nostr.postr.JsonFilter +import nostr.postr.events.TextNoteEvent +import nostr.postr.toHex + +object HomeNewThreadFeedFilter: FeedFilter() { + lateinit var account: Account + + override fun feed(): List { + val user = account.userProfile() + + return LocalCache.notes.values + .filter { + (it.event is TextNoteEvent || it.event is RepostEvent) + && it.author in user.follows + && account.isAcceptable(it) + && it.isNewThread() + } + .sortedBy { it.event?.createdAt } + .reversed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrNotificationDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt similarity index 84% rename from app/src/main/java/com/vitorpamplona/amethyst/service/NostrNotificationDataSource.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index e629d704e..662a4d624 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrNotificationDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -1,4 +1,4 @@ -package com.vitorpamplona.amethyst.service +package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note @@ -10,7 +10,7 @@ import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import nostr.postr.JsonFilter -object NostrNotificationDataSource: NostrDataSource("NotificationFeed") { +object NotificationFeedFilter: FeedFilter() { lateinit var account: Account override fun feed(): List { @@ -25,6 +25,4 @@ object NostrNotificationDataSource: NostrDataSource("NotificationFeed") { .sortedBy { it.event?.createdAt } .reversed() } - - override fun updateChannelFilters() {} } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt new file mode 100644 index 000000000..46740b416 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ThreadFeedFilter.kt @@ -0,0 +1,22 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.ThreadAssembler +import com.vitorpamplona.amethyst.service.NostrThreadDataSource + +object ThreadFeedFilter: FeedFilter() { + var noteId: String? = null + + override fun feed(): List { + val eventsToWatch = noteId?.let { ThreadAssembler().findThreadFor(it) } ?: emptySet() + // Currently orders by date of each event, descending, at each level of the reply stack + val order = compareByDescending { it.replyLevelSignature() } + + return eventsToWatch.sortedWith(order) + } + + fun loadThread(noteId: String?) { + this.noteId = noteId + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowersDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt similarity index 53% rename from app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowersDataSource.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt index 7d80368e4..81df81407 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowersDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowersFeedFilter.kt @@ -1,23 +1,19 @@ -package com.vitorpamplona.amethyst.service +package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import nostr.postr.JsonFilter -import nostr.postr.events.ContactListEvent -object NostrUserProfileFollowersDataSource: NostrDataSource("UserProfileFollowerFeed") { +object UserProfileFollowersFeedFilter: FeedFilter() { lateinit var account: Account var user: User? = null - fun loadUserProfile(userId: String) { + fun loadUserProfile(accountLoggedIn: Account, userId: String) { + account = accountLoggedIn user = LocalCache.users[userId] - resetFilters() } override fun feed(): List { return user?.followers?.filter { account.isAcceptable(it) } ?: emptyList() } - - override fun updateChannelFilters() {} } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowsDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt similarity index 54% rename from app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowsDataSource.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt index ba6244ea3..37fcfc77a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileFollowsDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileFollowsFeedFilter.kt @@ -1,23 +1,19 @@ -package com.vitorpamplona.amethyst.service +package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import nostr.postr.JsonFilter -import nostr.postr.events.ContactListEvent -object NostrUserProfileFollowsDataSource: NostrDataSource("UserProfileFollowsFeed") { +object UserProfileFollowsFeedFilter: FeedFilter() { lateinit var account: Account var user: User? = null - fun loadUserProfile(userId: String) { + fun loadUserProfile(accountLoggedIn: Account, userId: String) { + account = accountLoggedIn user = LocalCache.users[userId] - resetFilters() } override fun feed(): List { return user?.follows?.filter { account.isAcceptable(it) } ?: emptyList() } - - override fun updateChannelFilters() {} } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNoteFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNoteFeedFilter.kt new file mode 100644 index 000000000..dccff52b1 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNoteFeedFilter.kt @@ -0,0 +1,24 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User + +object UserProfileNoteFeedFilter: FeedFilter() { + lateinit var account: Account + var user: User? = null + + fun loadUserProfile(accountLoggedIn: Account, userId: String) { + account = accountLoggedIn + user = LocalCache.getOrCreateUser(userId) + } + + override fun feed(): List { + return user?.notes + ?.filter { account.isAcceptable(it) } + ?.sortedBy { it.event?.createdAt } + ?.reversed() + ?: emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileZapsDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt similarity index 56% rename from app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileZapsDataSource.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt index 3e01b7409..58ff6c0bb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileZapsDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt @@ -1,25 +1,18 @@ -package com.vitorpamplona.amethyst.service +package com.vitorpamplona.amethyst.ui.dal -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.LnZapEvent -import nostr.postr.JsonFilter -import nostr.postr.events.ContactListEvent -object NostrUserProfileZapsDataSource: NostrDataSource>("UserProfileZapsFeed") { - lateinit var account: Account +object UserProfileZapsFeedFilter: FeedFilter>() { var user: User? = null fun loadUserProfile(userId: String) { - user = LocalCache.users[userId] - resetFilters() + user = LocalCache.getOrCreateUser(userId) } override fun feed(): List> { return (user?.zaps?.filter { it.value != null }?.toList()?.sortedBy { (it.second?.event as? LnZapEvent)?.amount }?.reversed() ?: emptyList()) as List> } - - override fun updateChannelFilters() {} } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index 75677423c..87a6b5c48 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight @@ -16,12 +15,18 @@ import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.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 +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -29,7 +34,7 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.ui.note.NewItemsBubble +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch val bottomNavigationItems = listOf( @@ -40,7 +45,7 @@ val bottomNavigationItems = listOf( ) @Composable -fun AppBottomBar(navController: NavHostController) { +fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) { val currentRoute = currentRoute(navController) val coroutineScope = rememberCoroutineScope() @@ -55,7 +60,7 @@ fun AppBottomBar(navController: NavHostController) { ) { bottomNavigationItems.forEach { item -> BottomNavigationItem( - icon = { NotifiableIcon(item, currentRoute) }, + icon = { NotifiableIcon(item, currentRoute, accountViewModel) }, selected = currentRoute == item.route, onClick = { coroutineScope.launch { @@ -89,7 +94,7 @@ fun AppBottomBar(navController: NavHostController) { } @Composable -private fun NotifiableIcon(item: Route, currentRoute: String?) { +private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel: AccountViewModel) { Box(Modifier.size(if ("Home" == item.route) 25.dp else 23.dp)) { Icon( painter = painterResource(id = item.icon), @@ -98,39 +103,48 @@ private fun NotifiableIcon(item: Route, currentRoute: String?) { tint = if (currentRoute == item.route) MaterialTheme.colors.primary else Color.Unspecified ) + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + // Notification val dbState = LocalCache.live.observeAsState() + val db = dbState.value ?: return + val notifState = NotificationCache.live.observeAsState() + val notif = notifState.value ?: return - val db = dbState.value - val notif = notifState.value + var hasNewItems by remember { mutableStateOf(false) } - if (db != null && notif != null) { - if (item.hasNewItems(db.cache, notif.cache)) { + val context = LocalContext.current.applicationContext + + LaunchedEffect(key1 = notif) { + hasNewItems = item.hasNewItems(account, notif.cache, context) + } + + if (hasNewItems) { + Box( + Modifier + .width(10.dp) + .height(10.dp) + .align(Alignment.TopEnd) + ) { Box( - Modifier + modifier = Modifier .width(10.dp) .height(10.dp) - .align(Alignment.TopEnd) + .clip(shape = CircleShape) + .background(MaterialTheme.colors.primary), + contentAlignment = Alignment.TopEnd ) { - Box( + Text( + "", + color = Color.White, + textAlign = TextAlign.Center, + fontSize = 12.sp, modifier = Modifier - .width(10.dp) - .height(10.dp) - .clip(shape = CircleShape) - .background(MaterialTheme.colors.primary), - contentAlignment = Alignment.TopEnd - ) { - Text( - "", - color = Color.White, - textAlign = TextAlign.Center, - fontSize = 12.sp, - modifier = Modifier - .wrapContentHeight() - .align(Alignment.TopEnd) - ) - } + .wrapContentHeight() + .align(Alignment.TopEnd) + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index bb248ba9b..bac7d6299 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -1,7 +1,6 @@ package com.vitorpamplona.amethyst.ui.navigation import android.util.Log -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -36,45 +35,33 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import coil.Coil -import coil.compose.AsyncImage -import coil.compose.rememberAsyncImagePainter import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource -import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource +import com.vitorpamplona.amethyst.service.NostrChatroomDataSource import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.service.NostrHomeDataSource -import com.vitorpamplona.amethyst.service.NostrNotificationDataSource import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileZapsDataSource import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.actions.NewRelayListView -import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import java.net.URLEncoder import kotlinx.coroutines.launch @Composable @@ -141,12 +128,11 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) NostrAccountDataSource.printCounter() NostrChannelDataSource.printCounter() - NostrChatRoomDataSource.printCounter() + NostrChatroomDataSource.printCounter() NostrChatroomListDataSource.printCounter() NostrGlobalDataSource.printCounter() NostrHomeDataSource.printCounter() - NostrNotificationDataSource.printCounter() NostrSingleEventDataSource.printCounter() NostrSearchEventOrUserDataSource.printCounter() @@ -155,9 +141,6 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) NostrThreadDataSource.printCounter() NostrUserProfileDataSource.printCounter() - NostrUserProfileFollowersDataSource.printCounter() - NostrUserProfileFollowsDataSource.printCounter() - NostrUserProfileZapsDataSource.printCounter() println("Connected Relays: " + RelayPool.connectedRelays()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 92850707d..b6c0f881a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -1,7 +1,9 @@ package com.vitorpamplona.amethyst.ui.navigation +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.LocalContext import androidx.navigation.NamedNavArgument import androidx.navigation.NavBackStackEntry @@ -12,21 +14,16 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.navArgument import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource -import com.vitorpamplona.amethyst.service.NostrDataSource -import com.vitorpamplona.amethyst.service.NostrHomeDataSource -import com.vitorpamplona.amethyst.service.NostrNotificationDataSource -import com.vitorpamplona.amethyst.service.model.RepostEvent +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter +import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter +import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.ChannelScreen import com.vitorpamplona.amethyst.ui.screen.ChatroomListScreen import com.vitorpamplona.amethyst.ui.screen.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.FiltersScreen import com.vitorpamplona.amethyst.ui.screen.HomeScreen -import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.SearchScreen @@ -37,24 +34,24 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel sealed class Route( val route: String, val icon: Int, - val hasNewItems: @Composable (LocalCache, NotificationCache) -> Boolean = @Composable { _,_ -> false }, + val hasNewItems: (Account, NotificationCache, Context) -> Boolean = { _,_,_ -> false }, val arguments: List = emptyList(), val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit ) { object Home : Route("Home", R.drawable.ic_home, - hasNewItems = { _, cache -> homeHasNewItems(cache) }, + hasNewItems = { acc, cache, ctx -> homeHasNewItems(acc, cache, ctx) }, buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } } ) object Search : Route("Search", R.drawable.ic_search, buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) }} ) object Notification : Route("Notification", R.drawable.ic_notifications, - hasNewItems = { _, cache -> notificationHasNewItems(cache) }, + hasNewItems = { acc, cache, ctx -> notificationHasNewItems(acc, cache, ctx) }, buildScreen = { acc, accSt, nav -> { _ -> NotificationScreen(acc, nav) }} ) object Message : Route("Message", R.drawable.ic_dm, - hasNewItems = { _, cache -> messagesHasNewItems(cache) }, + hasNewItems = { acc, cache, ctx -> messagesHasNewItems(acc, cache, ctx) }, buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }} ) @@ -107,45 +104,32 @@ public fun currentRoute(navController: NavHostController): String? { return navBackStackEntry?.destination?.route } -@Composable -private fun homeHasNewItems(cache: NotificationCache): Boolean { - val context = LocalContext.current.applicationContext - val lastTimeFollows = cache.load("HomeFollows", context) +private fun homeHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean { + val lastTime = cache.load("HomeFollows", context) - val homeFeed = NostrHomeDataSource.feed().take(100) + HomeNewThreadFeedFilter.account = account - val hasNewInFollows = homeFeed.filter { - it.isNewThread() - }.filter { - (it.event?.createdAt ?: 0) > lastTimeFollows - }.isNotEmpty() - - return hasNewInFollows + return HomeNewThreadFeedFilter.feed().any {(it.event?.createdAt ?: 0) > lastTime } } -@Composable -private fun notificationHasNewItems(cache: NotificationCache): Boolean { - val context = LocalContext.current.applicationContext +private fun notificationHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean { val lastTime = cache.load("Notification", context) - return NostrNotificationDataSource.loadTop() - .filter { it.event != null && it.event!!.createdAt > lastTime } - .isNotEmpty() + + NotificationFeedFilter.account = account + + return NotificationFeedFilter.feed().any {(it.event?.createdAt ?: 0) > lastTime } } -@Composable -private fun messagesHasNewItems(cache: NotificationCache): Boolean { - val context = LocalContext.current.applicationContext - return NostrChatroomListDataSource.feed().take(100).filter { - // only for known sources - val me = NostrChatroomListDataSource.account.userProfile() - it.channel == null && me.hasSentMessagesTo(it.author) && it.author != me - }.filter { - val lastTime = if (it.channel != null) { - cache.load("Channel/${it.channel!!.idHex}", context) - } else { - cache.load("Room/${it.author?.pubkeyHex}", context) - } +private fun messagesHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean { + ChatroomListKnownFeedFilter.account = account - NostrChatroomListDataSource.account.isAcceptable(it) && it.event != null && it.event!!.createdAt > lastTime - }.isNotEmpty() + return ChatroomListKnownFeedFilter.feed().any { + if (it.channel == null) { + val lastTime = cache.load("Room/${it.author?.pubkeyHex}", context) + + (it.event?.createdAt ?: 0) > lastTime + } else { + false + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt index b45eda201..c669c9d62 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt @@ -1,30 +1,28 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme 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 androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import coil.compose.AsyncImage -import coil.compose.rememberAsyncImagePainter import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R @@ -32,8 +30,8 @@ import com.vitorpamplona.amethyst.ui.screen.BoostSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun BoostSetCompose(likeSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { - val noteState by likeSetCard.note.live.observeAsState() +fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { + val noteState by boostSetCard.note.live.observeAsState() val note = noteState?.note val accountState by accountViewModel.accountLiveData.observeAsState() @@ -44,8 +42,15 @@ fun BoostSetCompose(likeSetCard: BoostSetCard, isInnerNote: Boolean = false, rou if (note?.event == null) { BlankNote(Modifier, isInnerNote) } else { - val isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context) - NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context) + var isNew by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = routeForLastRead) { + isNew = boostSetCard.createdAt > NotificationCache.load(routeForLastRead, context) + + val createdAt = note.event?.createdAt + if (createdAt != null) + NotificationCache.markAsRead(routeForLastRead, boostSetCard.createdAt, context) + } Column( modifier = Modifier.background( @@ -75,7 +80,7 @@ fun BoostSetCompose(likeSetCard: BoostSetCard, isInnerNote: Boolean = false, rou Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) { FlowRow() { - likeSetCard.boostEvents.forEach { + boostSetCard.boostEvents.forEach { NoteAuthorPicture( note = it, navController = navController, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt index 9fa7f863f..5ce6c1bf6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt @@ -20,8 +20,12 @@ import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.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 androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -81,11 +85,13 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr noteEvent?.content } channel?.let { channel -> - val hasNewMessages = - if (noteEvent != null) - noteEvent.createdAt > notificationCache.cache.load("Channel/${channel.idHex}", context) - else - false + var hasNewMessages by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = notificationCache) { + noteEvent?.let { + hasNewMessages = it.createdAt > notificationCache.cache.load("Channel/${channel.idHex}", context) + } + } ChannelName( channelPicture = channel.profilePicture(), @@ -122,11 +128,13 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr val noteEvent = note.event userToComposeOn.let { user -> - val hasNewMessages = - if (noteEvent != null) - noteEvent.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context) - else - false + var hasNewMessages by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = notificationCache) { + noteEvent?.let { + hasNewMessages = it.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context) + } + } ChannelName( channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 76d78e8fc..c71436933 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -30,6 +30,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.ExpandMore 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 @@ -115,15 +116,18 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote shape = ChatBubbleShapeThem } - // Mark read - val isNew = routeForLastRead?.run { - val isNew = NotificationCache.load(this, context) + var isNew by remember { mutableStateOf(false) } - val createdAt = note.event?.createdAt - if (createdAt != null) - NotificationCache.markAsRead(this, createdAt, context) + LaunchedEffect(key1 = routeForLastRead) { + routeForLastRead?.let { + val lastTime = NotificationCache.load(it, context) - isNew + val createdAt = note.event?.createdAt + if (createdAt != null) { + NotificationCache.markAsRead(it, createdAt, context) + isNew = createdAt > lastTime + } + } } Column() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt index 67fc34288..9b6ca8b23 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt @@ -13,8 +13,12 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme 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 androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -44,8 +48,15 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn if (note == null) { BlankNote(Modifier, isInnerNote) } else { - val isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context) - NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context) + var isNew by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = routeForLastRead) { + isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context) + + val createdAt = note.event?.createdAt + if (createdAt != null) + NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context) + } Column( modifier = Modifier.background( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 7b35d47fe..7190a5771 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -31,6 +31,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.MoreVert 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 @@ -120,17 +121,19 @@ fun NoteCompose( onClick = { showHiddenNote = true } ) } else { - val isNew = routeForLastRead?.run { - val lastTime = NotificationCache.load(this, context) + var isNew by remember { mutableStateOf(false) } - val createdAt = note.event?.createdAt - if (createdAt != null) { - NotificationCache.markAsRead(this, createdAt, context) - createdAt > lastTime - } else { - false + LaunchedEffect(key1 = routeForLastRead) { + routeForLastRead?.let { + val lastTime = NotificationCache.load(it, context) + + val createdAt = note.event?.createdAt + if (createdAt != null) { + NotificationCache.markAsRead(it, createdAt, context) + isNew = createdAt > lastTime + } } - } ?: false + } Column(modifier = modifier.combinedClickable( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt index 937cc6ee8..e1b02d83d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt @@ -12,8 +12,12 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt 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 androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -42,8 +46,15 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, modifier: Modifier = Modifier, isInner if (note == null) { BlankNote(Modifier, isInnerNote) } else { - val isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead, context) - NotificationCache.markAsRead(routeForLastRead, zapSetCard.createdAt, context) + var isNew by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = routeForLastRead) { + isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead, context) + + val createdAt = note.event?.createdAt + if (createdAt != null) + NotificationCache.markAsRead(routeForLastRead, zapSetCard.createdAt, context) + } Column( modifier = Modifier.background( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 85e9e65db..29ca3f7dc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.screen +import android.util.Log import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -8,7 +9,9 @@ import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account import fr.acinq.secp256k1.Hex import java.util.regex.Pattern +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -22,11 +25,13 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences): Vie init { // pulls account from storage. - viewModelScope.launch(Dispatchers.IO) { - localPreferences.loadFromEncryptedStorage()?.let { - login(it) - } + + // Keeps it in the the UI thread to void blinking the login page. + //viewModelScope.launch(Dispatchers.IO) { + localPreferences.loadFromEncryptedStorage()?.let { + login(it) } + //} } fun login(key: String) { @@ -61,7 +66,8 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences): Vie else _accountContent.update { AccountState.LoggedInViewOnly ( account ) } - viewModelScope.launch(Dispatchers.IO) { + val scope = CoroutineScope(Job() + Dispatchers.IO) + scope.launch { ServiceManager.start(account) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index a1cacac3d..32b514a95 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -1,32 +1,30 @@ package com.vitorpamplona.amethyst.ui.screen -import android.os.Handler -import android.os.Looper import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.NostrDataSource import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RepostEvent +import com.vitorpamplona.amethyst.ui.dal.FeedFilter +import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -class CardFeedViewModel(val dataSource: NostrDataSource): ViewModel() { +object NotificationViewModel: CardFeedViewModel(NotificationFeedFilter) + +open class CardFeedViewModel(val dataSource: FeedFilter): ViewModel() { private val _feedContent = MutableStateFlow(CardFeedState.Loading) val feedContent = _feedContent.asStateFlow() @@ -125,8 +123,7 @@ class CardFeedViewModel(val dataSource: NostrDataSource): ViewModel() { handlerWaiting.set(true) val scope = CoroutineScope(Job() + Dispatchers.Default) scope.launch { - if (feedContent.value is CardFeedState.Loaded) - delay(5000) + delay(50) refresh() handlerWaiting.set(false) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 2652f6a2b..2df2d4f70 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -36,7 +36,12 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String?) { +fun FeedView( + viewModel: FeedViewModel, + accountViewModel: AccountViewModel, + navController: NavController, + routeForLastRead: String? +) { val feedState by viewModel.feedContent.collectAsState() var isRefreshing by remember { mutableStateOf(false) } @@ -44,7 +49,7 @@ fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navCo LaunchedEffect(isRefreshing) { if (isRefreshing) { - viewModel.hardRefresh() + viewModel.refresh() isRefreshing = false } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index a750ab2f4..8a1627604 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -2,18 +2,19 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.NostrChannelDataSource -import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource -import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource -import com.vitorpamplona.amethyst.service.NostrDataSource -import com.vitorpamplona.amethyst.service.NostrGlobalDataSource -import com.vitorpamplona.amethyst.service.NostrHomeDataSource -import com.vitorpamplona.amethyst.service.NostrThreadDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource +import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter +import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter +import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter +import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter +import com.vitorpamplona.amethyst.ui.dal.FeedFilter +import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter +import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter +import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter +import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter +import com.vitorpamplona.amethyst.ui.dal.UserProfileNoteFeedFilter import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -23,61 +24,28 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -class NostrChannelFeedViewModel: FeedViewModel(NostrChannelDataSource) -class NostrChatRoomFeedViewModel: FeedViewModel(NostrChatRoomDataSource) -class NostrGlobalFeedViewModel: FeedViewModel(NostrGlobalDataSource) -class NostrThreadFeedViewModel: FeedViewModel(NostrThreadDataSource) -class NostrUserProfileFeedViewModel: FeedViewModel(NostrUserProfileDataSource) - -class NostrChatroomListKnownFeedViewModel: FeedViewModel(NostrChatroomListDataSource) { - override fun newListFromDataSource(): List { - // Filter: all channels + PMs the account has replied to - return super.newListFromDataSource().filter { - val me = NostrChatroomListDataSource.account.userProfile() - it.channel != null || me.hasSentMessagesTo(it.author) - } - } -} -class NostrChatroomListNewFeedViewModel: FeedViewModel(NostrChatroomListDataSource) { - override fun newListFromDataSource(): List { - // Filter: no channels + PMs the account has never replied to - return super.newListFromDataSource().filter { - val me = NostrChatroomListDataSource.account.userProfile() - it.channel == null && !me.hasSentMessagesTo(it.author) - } - } -} - -class NostrHomeFeedViewModel: FeedViewModel(NostrHomeDataSource) { - override fun newListFromDataSource(): List { - // Filter: no replies - return dataSource.feed().filter { it.isNewThread() }.take(1000) - } -} - -class NostrHomeRepliesFeedViewModel: FeedViewModel(NostrHomeDataSource) { - override fun newListFromDataSource(): List { - // Filter: only replies - return dataSource.feed().filter {! it.isNewThread() }.take(1000) - } -} +class NostrChannelFeedViewModel: FeedViewModel(ChannelFeedFilter) +class NostrChatRoomFeedViewModel: FeedViewModel(ChatroomFeedFilter) +class NostrGlobalFeedViewModel: FeedViewModel(GlobalFeedFilter) +class NostrThreadFeedViewModel: FeedViewModel(ThreadFeedFilter) +class NostrUserProfileFeedViewModel: FeedViewModel(UserProfileNoteFeedFilter) +class NostrChatroomListKnownFeedViewModel: FeedViewModel(ChatroomListKnownFeedFilter) +class NostrChatroomListNewFeedViewModel: FeedViewModel(ChatroomListNewFeedFilter) +class NostrHomeFeedViewModel: FeedViewModel(HomeNewThreadFeedFilter) +class NostrHomeRepliesFeedViewModel: FeedViewModel(HomeConversationsFeedFilter) -abstract class FeedViewModel(val dataSource: NostrDataSource): ViewModel() { +abstract class FeedViewModel(val localFilter: FeedFilter): ViewModel() { private val _feedContent = MutableStateFlow(FeedState.Loading) val feedContent = _feedContent.asStateFlow() open fun newListFromDataSource(): List { - return dataSource.loadTop() - } - - fun hardRefresh() { - dataSource.resetFilters() + return localFilter.loadTop() } fun refresh() { + println("Model Refresh: ${this::class.simpleName}") val scope = CoroutineScope(Job() + Dispatchers.Default) scope.launch { refreshSuspended() @@ -90,7 +58,7 @@ abstract class FeedViewModel(val dataSource: NostrDataSource): ViewModel() val oldNotesState = feedContent.value if (oldNotesState is FeedState.Loaded) { // Using size as a proxy for has changed. - if (notes.size != oldNotesState.feed.value.size && notes.firstOrNull() != oldNotesState.feed.value.firstOrNull()) { + if (notes.size != oldNotesState.feed.value.size || notes.firstOrNull() != oldNotesState.feed.value.firstOrNull()) { updateFeed(notes) } } else { @@ -116,14 +84,13 @@ abstract class FeedViewModel(val dataSource: NostrDataSource): ViewModel() private var handlerWaiting = AtomicBoolean() @Synchronized - private fun invalidateData() { + fun invalidateData() { if (handlerWaiting.getAndSet(true)) return handlerWaiting.set(true) val scope = CoroutineScope(Job() + Dispatchers.Default) scope.launch { - if (feedContent.value is FeedState.Loaded) - delay(5000) + delay(50) refresh() handlerWaiting.set(false) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt index 12c195bc5..4c7ada516 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt @@ -2,16 +2,14 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.NostrDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileZapsDataSource +import com.vitorpamplona.amethyst.ui.dal.FeedFilter +import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -20,9 +18,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.atomic.AtomicBoolean -class NostrUserProfileZapsFeedViewModel: LnZapFeedViewModel(NostrUserProfileZapsDataSource) +class NostrUserProfileZapsFeedViewModel: LnZapFeedViewModel(UserProfileZapsFeedFilter) -open class LnZapFeedViewModel(val dataSource: NostrDataSource>): ViewModel() { +open class LnZapFeedViewModel(val dataSource: FeedFilter>): ViewModel() { private val _feedContent = MutableStateFlow(LnZapFeedState.Loading) val feedContent = _feedContent.asStateFlow() @@ -33,14 +31,13 @@ open class LnZapFeedViewModel(val dataSource: NostrDataSource>) } } - private fun refreshSuspended() { val notes = dataSource.loadTop() val oldNotesState = feedContent.value if (oldNotesState is LnZapFeedState.Loaded) { // Using size as a proxy for has changed. - if (notes.size != oldNotesState.feed.value.size && notes.firstOrNull() != oldNotesState.feed.value.firstOrNull()) { + if (notes.size != oldNotesState.feed.value.size || notes.firstOrNull() != oldNotesState.feed.value.firstOrNull()) { updateFeed(notes) } } else { @@ -72,7 +69,7 @@ open class LnZapFeedViewModel(val dataSource: NostrDataSource>) handlerWaiting.set(true) val scope = CoroutineScope(Job() + Dispatchers.Default) scope.launch { - delay(1000) + delay(50) refresh() handlerWaiting.set(false) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index d417734f4..e629f2915 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -1,43 +1,29 @@ package com.vitorpamplona.amethyst.ui.screen -import android.os.Handler -import android.os.Looper import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCacheState -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.NostrDataSource -import com.vitorpamplona.amethyst.service.NostrHiddenAccountsDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource +import com.vitorpamplona.amethyst.ui.dal.FeedFilter +import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter +import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter +import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.util.concurrent.atomic.AtomicBoolean -class NostrUserProfileFollowsUserFeedViewModel(): UserFeedViewModel( - NostrUserProfileFollowsDataSource -) +class NostrUserProfileFollowsUserFeedViewModel: UserFeedViewModel(UserProfileFollowsFeedFilter) +class NostrUserProfileFollowersUserFeedViewModel: UserFeedViewModel(UserProfileFollowersFeedFilter) +class NostrHiddenAccountsFeedViewModel: UserFeedViewModel(HiddenAccountsFeedFilter) -class NostrUserProfileFollowersUserFeedViewModel(): UserFeedViewModel( - NostrUserProfileFollowersDataSource -) - -class NostrHiddenAccountsFeedViewModel(): UserFeedViewModel( - NostrHiddenAccountsDataSource -) - -open class UserFeedViewModel(val dataSource: NostrDataSource): ViewModel() { +open class UserFeedViewModel(val dataSource: FeedFilter): ViewModel() { private val _feedContent = MutableStateFlow(UserFeedState.Loading) val feedContent = _feedContent.asStateFlow() @@ -48,14 +34,13 @@ open class UserFeedViewModel(val dataSource: NostrDataSource): ViewModel() } } - private fun refreshSuspended() { val notes = dataSource.loadTop() val oldNotesState = feedContent.value if (oldNotesState is UserFeedState.Loaded) { // Using size as a proxy for has changed. - if (notes.size != oldNotesState.feed.value.size && notes.firstOrNull() != oldNotesState.feed.value.firstOrNull()) { + if (notes.size != oldNotesState.feed.value.size || notes.firstOrNull() != oldNotesState.feed.value.firstOrNull()) { updateFeed(notes) } } else { @@ -87,8 +72,7 @@ open class UserFeedViewModel(val dataSource: NostrDataSource): ViewModel() handlerWaiting.set(true) val scope = CoroutineScope(Job() + Dispatchers.Default) scope.launch { - if (feedContent.value is UserFeedState.Loaded) - delay(5000) + delay(50) refresh() handlerWaiting.set(false) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 0527b1426..5144cf674 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.Share import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -42,6 +43,7 @@ import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -51,6 +53,8 @@ import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil.compose.AsyncImage @@ -62,11 +66,13 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.toNote import com.vitorpamplona.amethyst.service.NostrChannelDataSource +import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.actions.NewChannelView import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.actions.PostButton +import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import nostr.postr.toNpub @@ -79,15 +85,36 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun if (account != null && channelId != null) { val newPost = remember { mutableStateOf(TextFieldValue("")) } + ChannelFeedFilter.loadMessagesBetween(account, channelId) NostrChannelDataSource.loadMessagesBetween(channelId) val channelState by NostrChannelDataSource.channel!!.live.observeAsState() val channel = channelState?.channel ?: return val feedViewModel: NostrChannelFeedViewModel = viewModel() + val lifeCycleOwner = LocalLifecycleOwner.current LaunchedEffect(Unit) { - feedViewModel.refresh() + feedViewModel.invalidateData() + } + + DisposableEffect(channelId) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + println("Channel Start") + NostrChannelDataSource.start() + feedViewModel.invalidateData() + } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Channel Stop") + NostrChannelDataSource.stop() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } } Column(Modifier.fillMaxHeight()) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 6b27529a8..d80dfc726 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -9,12 +9,16 @@ import androidx.compose.material.TabRow import androidx.compose.material.TabRowDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect 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.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.google.accompanist.pager.ExperimentalPagerApi @@ -22,6 +26,10 @@ import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.rememberPagerState import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource +import com.vitorpamplona.amethyst.service.NostrHomeDataSource +import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter +import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch @@ -72,11 +80,35 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon @Composable fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + ChatroomListKnownFeedFilter.account = account val feedViewModel: NostrChatroomListKnownFeedViewModel = viewModel() LaunchedEffect(Unit) { - feedViewModel.hardRefresh() // refresh filters - feedViewModel.refresh() // refresh view + NostrChatroomListDataSource.resetFilters() + feedViewModel.invalidateData() + } + + val lifeCycleOwner = LocalLifecycleOwner.current + DisposableEffect(accountViewModel) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + println("Global Start") + NostrChatroomListDataSource.start() + feedViewModel.invalidateData() + } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Global Stop") + NostrChatroomListDataSource.stop() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } } Column(Modifier.fillMaxHeight()) { @@ -90,13 +122,37 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { @Composable fun TabNew(accountViewModel: AccountViewModel, navController: NavController) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + ChatroomListNewFeedFilter.account = account val feedViewModel: NostrChatroomListNewFeedViewModel = viewModel() LaunchedEffect(Unit) { - feedViewModel.hardRefresh() // refresh filters + NostrChatroomListDataSource.resetFilters() feedViewModel.refresh() // refresh view } + val lifeCycleOwner = LocalLifecycleOwner.current + DisposableEffect(accountViewModel) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + println("Global Start") + NostrChatroomListDataSource.start() + feedViewModel.invalidateData() + } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Global Stop") + NostrChatroomListDataSource.stop() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } + } + Column(Modifier.fillMaxHeight()) { Column( modifier = Modifier.padding(vertical = 0.dp) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 4e5caa917..12b93b6bc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -19,35 +19,36 @@ import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect 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 androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController -import coil.compose.AsyncImage -import coil.compose.rememberAsyncImagePainter import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource +import com.vitorpamplona.amethyst.service.NostrChannelDataSource +import com.vitorpamplona.amethyst.service.NostrChatroomDataSource +import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.actions.PostButton -import com.vitorpamplona.amethyst.ui.note.UserPicture +import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -59,16 +60,37 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr if (account != null && userId != null) { val newPost = remember { mutableStateOf(TextFieldValue("")) } - NostrChatRoomDataSource.loadMessagesBetween(account, userId) + ChatroomFeedFilter.loadMessagesBetween(account, userId) + NostrChatroomDataSource.loadMessagesBetween(account, userId) val feedViewModel: NostrChatRoomFeedViewModel = viewModel() + val lifeCycleOwner = LocalLifecycleOwner.current LaunchedEffect(Unit) { - feedViewModel.refresh() + feedViewModel.invalidateData() + } + + DisposableEffect(userId) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + println("Private Message Start") + NostrChatroomDataSource.start() + feedViewModel.invalidateData() + } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Private Message Stop") + NostrChatroomDataSource.stop() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } } Column(Modifier.fillMaxHeight()) { - NostrChatRoomDataSource.withUser?.let { + NostrChatroomDataSource.withUser?.let { ChatroomHeader( it, accountViewModel = accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/FiltersScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/FiltersScreen.kt index 2b69b1288..4f3756eba 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/FiltersScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/FiltersScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.material.TabRow import androidx.compose.material.TabRowDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.rememberCoroutineScope @@ -21,8 +20,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.rememberPagerState -import com.vitorpamplona.amethyst.service.NostrHiddenAccountsDataSource -import com.vitorpamplona.amethyst.service.NostrThreadDataSource +import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch @@ -33,7 +31,7 @@ fun FiltersScreen(accountViewModel: AccountViewModel, navController: NavControll val account = accountState?.account if (account != null) { - NostrHiddenAccountsDataSource.account = account + HiddenAccountsFeedFilter.account = account val feedViewModel: NostrHiddenAccountsFeedViewModel = viewModel() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index c55840006..45ab73fe8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -9,16 +9,26 @@ import androidx.compose.material.TabRow import androidx.compose.material.TabRowDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect 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.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.rememberPagerState +import com.vitorpamplona.amethyst.service.NostrGlobalDataSource +import com.vitorpamplona.amethyst.service.NostrHomeDataSource +import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter +import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch @@ -26,6 +36,12 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalPagerApi::class) @Composable fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + HomeNewThreadFeedFilter.account = account + HomeConversationsFeedFilter.account = account + val feedViewModel: NostrHomeFeedViewModel = viewModel() val feedViewModelReplies: NostrHomeRepliesFeedViewModel = viewModel() @@ -33,8 +49,31 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) val coroutineScope = rememberCoroutineScope() LaunchedEffect(Unit) { - feedViewModel.refresh() - feedViewModelReplies.refresh() + NostrHomeDataSource.resetFilters() + + feedViewModel.invalidateData() + feedViewModelReplies.invalidateData() + } + + val lifeCycleOwner = LocalLifecycleOwner.current + DisposableEffect(accountViewModel) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + println("Global Start") + NostrHomeDataSource.start() + feedViewModel.invalidateData() + feedViewModelReplies.invalidateData() + } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Global Stop") + NostrHomeDataSource.stop() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } } Column(Modifier.fillMaxHeight()) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 37ade2832..f0ee8c4c0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -38,7 +38,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun .background(MaterialTheme.colors.primaryVariant) .statusBarsPadding(), bottomBar = { - AppBottomBar(navController) + AppBottomBar(navController, accountViewModel) }, topBar = { AppTopBar(navController, scaffoldState, accountViewModel) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index f122e7e93..d5da493b2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -11,13 +11,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController -import com.vitorpamplona.amethyst.service.NostrNotificationDataSource +import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavController) { - val feedViewModel: CardFeedViewModel = viewModel { CardFeedViewModel( NostrNotificationDataSource ) } + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + NotificationFeedFilter.account = account + val feedViewModel: NotificationViewModel = viewModel() LaunchedEffect(Unit) { feedViewModel.refresh() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index c41113bf7..39c733486 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -1,11 +1,8 @@ package com.vitorpamplona.amethyst.ui.screen -import android.content.Intent -import android.net.Uri import androidx.compose.foundation.* import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.* -import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText @@ -40,31 +37,28 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController -import coil.compose.AsyncImage import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.rememberPagerState import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileZapsDataSource import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView import com.vitorpamplona.amethyst.ui.components.InvoiceRequest +import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter +import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter +import com.vitorpamplona.amethyst.ui.dal.UserProfileNoteFeedFilter +import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -81,10 +75,12 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro if (userId == null) return + UserProfileNoteFeedFilter.loadUserProfile(account, userId) + UserProfileFollowersFeedFilter.loadUserProfile(account, userId) + UserProfileFollowsFeedFilter.loadUserProfile(account, userId) + UserProfileZapsFeedFilter.loadUserProfile(userId) + NostrUserProfileDataSource.loadUserProfile(userId) - NostrUserProfileFollowersDataSource.loadUserProfile(userId) - NostrUserProfileFollowsDataSource.loadUserProfile(userId) - NostrUserProfileZapsDataSource.loadUserProfile(userId) val lifeCycleOwner = LocalLifecycleOwner.current @@ -92,17 +88,13 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_RESUME) { println("Profile Start") + NostrUserProfileDataSource.loadUserProfile(userId) NostrUserProfileDataSource.start() - NostrUserProfileFollowersDataSource.start() - NostrUserProfileFollowsDataSource.start() - NostrUserProfileZapsDataSource.start() } if (event == Lifecycle.Event.ON_PAUSE) { println("Profile Stop") + NostrUserProfileDataSource.loadUserProfile(null) NostrUserProfileDataSource.stop() - NostrUserProfileFollowsDataSource.stop() - NostrUserProfileFollowersDataSource.stop() - NostrUserProfileZapsDataSource.stop() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 5b2be7bc5..bf2487f81 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource +import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter import com.vitorpamplona.amethyst.ui.note.ChannelName import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.UserCompose @@ -76,11 +77,16 @@ import kotlinx.coroutines.withContext @Composable fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + GlobalFeedFilter.account = account + NostrGlobalDataSource.account = account val feedViewModel: NostrGlobalFeedViewModel = viewModel() val lifeCycleOwner = LocalLifecycleOwner.current LaunchedEffect(Unit) { - feedViewModel.refresh() + feedViewModel.invalidateData() } DisposableEffect(accountViewModel) { @@ -88,6 +94,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle if (event == Lifecycle.Event.ON_RESUME) { println("Global Start") NostrGlobalDataSource.start() + feedViewModel.invalidateData() } if (event == Lifecycle.Event.ON_PAUSE) { println("Global Stop") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt index 0c121848d..8bc2384ec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ThreadScreen.kt @@ -16,10 +16,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.vitorpamplona.amethyst.service.NostrThreadDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileZapsDataSource +import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable @@ -28,31 +25,36 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, navControl val lifeCycleOwner = LocalLifecycleOwner.current - DisposableEffect(accountViewModel) { - val observer = LifecycleEventObserver { source, event -> - if (event == Lifecycle.Event.ON_RESUME) { - println("Thread Start") - NostrThreadDataSource.start() - } - if (event == Lifecycle.Event.ON_PAUSE) { - println("Thread Stop") - NostrThreadDataSource.stop() - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { - lifeCycleOwner.lifecycle.removeObserver(observer) - } - } - if (account != null && noteId != null) { + ThreadFeedFilter.loadThread(noteId) NostrThreadDataSource.loadThread(noteId) - val feedViewModel: NostrThreadFeedViewModel = viewModel() LaunchedEffect(Unit) { - feedViewModel.refresh() + feedViewModel.invalidateData() + } + + DisposableEffect(accountViewModel) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + println("Thread Start") + ThreadFeedFilter.loadThread(noteId) + NostrThreadDataSource.loadThread(noteId) + NostrThreadDataSource.start() + feedViewModel.invalidateData() + } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Thread Stop") + ThreadFeedFilter.loadThread(null) + NostrThreadDataSource.loadThread(null) + NostrThreadDataSource.stop() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } } Column(Modifier.fillMaxHeight()) {