Massive Refactoring:

1. Split between Relay Filters and Screen Filters.
2. Moving Notification dots to background threads.
3. Loading new posts on ThreadView on the fly.
This commit is contained in:
Vitor Pamplona
2023-02-18 13:06:53 -05:00
parent f0e09197ff
commit 4f53a74004
59 changed files with 832 additions and 592 deletions
@@ -7,14 +7,11 @@ import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource 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 import com.vitorpamplona.amethyst.service.relays.Client
object ServiceManager { object ServiceManager {
@@ -34,25 +31,18 @@ object ServiceManager {
// start services // start services
NostrAccountDataSource.account = myAccount NostrAccountDataSource.account = myAccount
NostrHomeDataSource.account = myAccount NostrHomeDataSource.account = myAccount
NostrNotificationDataSource.account = myAccount
NostrChatroomListDataSource.account = myAccount NostrChatroomListDataSource.account = myAccount
NostrGlobalDataSource.account = myAccount NostrGlobalDataSource.account = myAccount
NostrChannelDataSource.account = myAccount
NostrUserProfileDataSource.account = myAccount
NostrUserProfileFollowsDataSource.account = myAccount
NostrUserProfileFollowersDataSource.account = myAccount
// Notification Elements
NostrAccountDataSource.start() NostrAccountDataSource.start()
//NostrGlobalDataSource.start()
NostrHomeDataSource.start() NostrHomeDataSource.start()
NostrNotificationDataSource.start() NostrChatroomListDataSource.start()
// More Info Data Sources
NostrSingleEventDataSource.start() NostrSingleEventDataSource.start()
NostrSingleChannelDataSource.start() NostrSingleChannelDataSource.start()
NostrSingleUserDataSource.start() NostrSingleUserDataSource.start()
//NostrThreadDataSource.start()
NostrChatroomListDataSource.start()
} else { } else {
// if not logged in yet, start a basic service wit default relays // if not logged in yet, start a basic service wit default relays
Client.connect(Constants.convertDefaultRelays()) Client.connect(Constants.convertDefaultRelays())
@@ -65,11 +55,8 @@ object ServiceManager {
NostrChannelDataSource.stop() NostrChannelDataSource.stop()
NostrChatroomListDataSource.stop() NostrChatroomListDataSource.stop()
NostrUserProfileDataSource.stop() NostrUserProfileDataSource.stop()
NostrUserProfileFollowersDataSource.stop()
NostrUserProfileFollowsDataSource.stop()
NostrGlobalDataSource.stop() NostrGlobalDataSource.stop()
NostrNotificationDataSource.stop()
NostrSingleChannelDataSource.stop() NostrSingleChannelDataSource.stop()
NostrSingleEventDataSource.stop() NostrSingleEventDataSource.stop()
NostrSingleUserDataSource.stop() NostrSingleUserDataSource.stop()
@@ -1,10 +1,8 @@
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import android.content.res.Resources import android.content.res.Resources
import android.util.Log
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
@@ -406,14 +404,12 @@ class Account(
} }
init { init {
GlobalScope.launch(Dispatchers.Main) {
userProfile().liveRelays.observeForever { userProfile().liveRelays.observeForever {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
reconnectIfRelaysHaveChanged() reconnectIfRelaysHaveChanged()
} }
} }
} }
}
// Observers line up here. // Observers line up here.
val live: AccountLiveData = AccountLiveData(this) val live: AccountLiveData = AccountLiveData(this)
@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
@@ -60,12 +61,12 @@ class ChannelLiveData(val channel: Channel): LiveData<ChannelState>(ChannelState
override fun onActive() { override fun onActive() {
super.onActive() super.onActive()
NostrSingleEventDataSource.add(channel.idHex) NostrSingleChannelDataSource.add(channel.idHex)
} }
override fun onInactive() { override fun onInactive() {
super.onInactive() super.onInactive()
NostrSingleEventDataSource.remove(channel.idHex) NostrSingleChannelDataSource.remove(channel.idHex)
} }
} }
@@ -47,7 +47,6 @@ object LocalCache {
val users = ConcurrentHashMap<HexKey, User>() val users = ConcurrentHashMap<HexKey, User>()
val notes = ConcurrentHashMap<HexKey, Note>() val notes = ConcurrentHashMap<HexKey, Note>()
val channels = ConcurrentHashMap<HexKey, Channel>() val channels = ConcurrentHashMap<HexKey, Channel>()
@Synchronized @Synchronized
fun getOrCreateUser(key: HexKey): User { fun getOrCreateUser(key: HexKey): User {
return users[key] ?: run { return users[key] ?: run {
@@ -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<Note> = 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<Note> {
val (result, elapsed) = measureTimedValue {
val note = LocalCache.getOrCreateNote(noteId)
if (note.event != null) {
val thread = mutableListOf<Note>()
val threadSet = mutableSetOf<Note>()
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<Note>, threadSet: MutableSet<Note>) {
if (note !in threadSet) {
thread.add(note)
threadSet.add(note)
note.replies.forEach {
loadDown(it, thread, threadSet)
}
}
}
}
@@ -15,7 +15,7 @@ import nostr.postr.events.ContactListEvent
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
object NostrAccountDataSource: NostrDataSource<Note>("AccountData") { object NostrAccountDataSource: NostrDataSource("AccountData") {
lateinit var account: Account lateinit var account: Account
fun createAccountContactListFilter(): TypedFilter { fun createAccountContactListFilter(): TypedFilter {
@@ -63,15 +63,6 @@ object NostrAccountDataSource: NostrDataSource<Note>("AccountData") {
val accountChannel = requestNewChannel() val accountChannel = requestNewChannel()
override fun feed(): List<Note> {
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() { override fun updateChannelFilters() {
// gets everthing about the user logged in // gets everthing about the user logged in
accountChannel.typedFilters = listOf( accountChannel.typedFilters = listOf(
@@ -1,6 +1,6 @@
package com.vitorpamplona.amethyst.service 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.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent 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 com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
object NostrChannelDataSource: NostrDataSource<Note>("ChatroomFeed") { object NostrChannelDataSource: NostrDataSource("ChatroomFeed") {
lateinit var account: Account var channel: Channel? = null
var channel: com.vitorpamplona.amethyst.model.Channel? = null
fun loadMessagesBetween(channelId: String) { fun loadMessagesBetween(channelId: String) {
channel = LocalCache.getOrCreateChannel(channelId) channel = LocalCache.getOrCreateChannel(channelId)
@@ -33,11 +32,6 @@ object NostrChannelDataSource: NostrDataSource<Note>("ChatroomFeed") {
val messagesChannel = requestNewChannel() val messagesChannel = requestNewChannel()
// returns the last Note of each user.
override fun feed(): List<Note> {
return channel?.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList()
}
override fun updateChannelFilters() { override fun updateChannelFilters() {
messagesChannel.typedFilters = listOfNotNull(createMessagesToChannelFilter()).ifEmpty { null } messagesChannel.typedFilters = listOfNotNull(createMessagesToChannelFilter()).ifEmpty { null }
} }
@@ -9,13 +9,14 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.PrivateDmEvent import nostr.postr.events.PrivateDmEvent
object NostrChatRoomDataSource: NostrDataSource<Note>("ChatroomFeed") { object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") {
lateinit var account: Account lateinit var account: Account
var withUser: User? = null var withUser: User? = null
fun loadMessagesBetween(accountIn: Account, userId: String) { fun loadMessagesBetween(accountIn: Account, userId: String) {
account = accountIn account = accountIn
withUser = LocalCache.users[userId] withUser = LocalCache.users[userId]
resetFilters()
} }
fun createMessagesToMeFilter(): TypedFilter? { fun createMessagesToMeFilter(): TypedFilter? {
@@ -40,7 +41,7 @@ object NostrChatRoomDataSource: NostrDataSource<Note>("ChatroomFeed") {
return if (myPeer != null) { return if (myPeer != null) {
TypedFilter( TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PRIVATE_DMS),
filter = JsonFilter( filter = JsonFilter(
kinds = listOf(PrivateDmEvent.kind), kinds = listOf(PrivateDmEvent.kind),
authors = listOf(account.userProfile().pubkeyHex), authors = listOf(account.userProfile().pubkeyHex),
@@ -54,13 +55,6 @@ object NostrChatRoomDataSource: NostrDataSource<Note>("ChatroomFeed") {
val inandoutChannel = requestNewChannel() val inandoutChannel = requestNewChannel()
// returns the last Note of each user.
override fun feed(): List<Note> {
val messages = account.userProfile().privateChatrooms[withUser] ?: return emptyList()
return messages.roomMessages.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed()
}
override fun updateChannelFilters() { override fun updateChannelFilters() {
inandoutChannel.typedFilters = listOfNotNull(createMessagesToMeFilter(), createMessagesFromMeFilter()).ifEmpty { null } inandoutChannel.typedFilters = listOfNotNull(createMessagesToMeFilter(), createMessagesFromMeFilter()).ifEmpty { null }
} }
@@ -10,7 +10,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.PrivateDmEvent import nostr.postr.events.PrivateDmEvent
object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") { object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") {
lateinit var account: Account lateinit var account: Account
fun createMessagesToMeFilter() = TypedFilter( fun createMessagesToMeFilter() = TypedFilter(
@@ -73,22 +73,6 @@ object NostrChatroomListDataSource: NostrDataSource<Note>("MailBoxFeed") {
val chatroomListChannel = requestNewChannel() val chatroomListChannel = requestNewChannel()
// returns the last Note of each user.
override fun feed(): List<Note> {
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() { override fun updateChannelFilters() {
val list = listOf( val list = listOf(
createMessagesToMeFilter(), createMessagesToMeFilter(),
@@ -33,9 +33,8 @@ import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.RecommendRelayEvent import nostr.postr.events.RecommendRelayEvent
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
abstract class NostrDataSource<T>(val debugName: String) { abstract class NostrDataSource(val debugName: String) {
private var subscriptions = mapOf<String, Subscription>() private var subscriptions = mapOf<String, Subscription>()
data class Counter(var counter:Int) data class Counter(var counter:Int)
private var eventCounter = mapOf<String, Counter>() private var eventCounter = mapOf<String, Counter>()
@@ -139,26 +138,6 @@ abstract class NostrDataSource<T>(val debugName: String) {
} }
} }
fun loadTop(): List<T> {
val returningList = feed().take(1000)
// prepare previews
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
loadPreviews(returningList)
}
return returningList
}
fun loadPreviews(list: List<T>) {
list.forEach {
if (it is Note) {
UrlCachedPreviewer.preloadPreviewsFor(it)
}
}
}
fun requestNewChannel(onEOSE: ((Long) -> Unit)? = null): Subscription { fun requestNewChannel(onEOSE: ((Long) -> Unit)? = null): Subscription {
val newSubscription = Subscription(UUID.randomUUID().toString().substring(0,4), onEOSE) val newSubscription = Subscription(UUID.randomUUID().toString().substring(0,4), onEOSE)
subscriptions = subscriptions + Pair(newSubscription.id, newSubscription) subscriptions = subscriptions + Pair(newSubscription.id, newSubscription)
@@ -231,5 +210,4 @@ abstract class NostrDataSource<T>(val debugName: String) {
} }
abstract fun updateChannelFilters() abstract fun updateChannelFilters()
abstract fun feed(): List<T>
} }
@@ -9,7 +9,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
object NostrGlobalDataSource: NostrDataSource<Note>("GlobalFeed") { object NostrGlobalDataSource: NostrDataSource("GlobalFeed") {
lateinit var account: Account lateinit var account: Account
fun createGlobalFilter() = TypedFilter( fun createGlobalFilter() = TypedFilter(
types = setOf(FeedType.GLOBAL), types = setOf(FeedType.GLOBAL),
@@ -21,15 +21,6 @@ object NostrGlobalDataSource: NostrDataSource<Note>("GlobalFeed") {
val globalFeedChannel = requestNewChannel() 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() { override fun updateChannelFilters() {
globalFeedChannel.typedFilters = listOf(createGlobalFilter()).ifEmpty { null } globalFeedChannel.typedFilters = listOf(createGlobalFilter()).ifEmpty { null }
} }
@@ -16,7 +16,7 @@ import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
import nostr.postr.toHex import nostr.postr.toHex
object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") { object NostrHomeDataSource: NostrDataSource("HomeFeed") {
lateinit var account: Account lateinit var account: Account
private val cacheListener: (UserState) -> Unit = { private val cacheListener: (UserState) -> Unit = {
@@ -62,16 +62,6 @@ object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
val followAccountChannel = requestNewChannel() val followAccountChannel = requestNewChannel()
override fun feed(): List<Note> {
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() { override fun updateChannelFilters() {
followAccountChannel.typedFilters = listOf(createFollowAccountsFilter()).ifEmpty { null } followAccountChannel.typedFilters = listOf(createFollowAccountsFilter()).ifEmpty { null }
} }
@@ -11,7 +11,7 @@ import nostr.postr.events.MetadataEvent
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
import nostr.postr.toHex import nostr.postr.toHex
object NostrSearchEventOrUserDataSource: NostrDataSource<Note>("SingleEventFeed") { object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") {
private var hexToWatch: String? = null private var hexToWatch: String? = null
private fun createAnythingWithIDFilter(): List<TypedFilter>? { private fun createAnythingWithIDFilter(): List<TypedFilter>? {
@@ -39,10 +39,6 @@ object NostrSearchEventOrUserDataSource: NostrDataSource<Note>("SingleEventFeed"
val searchChannel = requestNewChannel() val searchChannel = requestNewChannel()
override fun feed(): List<Note> {
return emptyList<Note>()
}
override fun updateChannelFilters() { override fun updateChannelFilters() {
searchChannel.typedFilters = createAnythingWithIDFilter() searchChannel.typedFilters = createAnythingWithIDFilter()
} }
@@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
object NostrSingleChannelDataSource: NostrDataSource<Note>("SingleChannelFeed") { object NostrSingleChannelDataSource: NostrDataSource("SingleChannelFeed") {
private var channelsToWatch = setOf<String>() private var channelsToWatch = setOf<String>()
private fun createRepliesAndReactionsFilter(): TypedFilter? { private fun createRepliesAndReactionsFilter(): TypedFilter? {
@@ -51,10 +51,6 @@ object NostrSingleChannelDataSource: NostrDataSource<Note>("SingleChannelFeed")
val singleChannelChannel = requestNewChannel() val singleChannelChannel = requestNewChannel()
override fun feed(): List<Note> {
return emptyList()
}
override fun updateChannelFilters() { override fun updateChannelFilters() {
val reactions = createRepliesAndReactionsFilter() val reactions = createRepliesAndReactionsFilter()
val missing = createLoadEventsIfNotLoadedFilter() val missing = createLoadEventsIfNotLoadedFilter()
@@ -15,7 +15,7 @@ import java.util.Date
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") { object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
private var eventsToWatch = setOf<String>() private var eventsToWatch = setOf<String>()
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? { private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
@@ -87,14 +87,6 @@ object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") {
invalidateFilters() invalidateFilters()
} }
override fun feed(): List<Note> {
return synchronized(eventsToWatch) {
eventsToWatch.map {
LocalCache.notes[it]
}.filterNotNull()
}
}
override fun updateChannelFilters() { override fun updateChannelFilters() {
val reactions = createRepliesAndReactionsFilter() val reactions = createRepliesAndReactionsFilter()
val missing = createLoadEventsIfNotLoadedFilter() val missing = createLoadEventsIfNotLoadedFilter()
@@ -8,7 +8,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
object NostrSingleUserDataSource: NostrDataSource<User>("SingleUserFeed") { object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
var usersToWatch = setOf<String>() var usersToWatch = setOf<String>()
fun createUserFilter(): List<TypedFilter>? { fun createUserFilter(): List<TypedFilter>? {
@@ -46,14 +46,6 @@ object NostrSingleUserDataSource: NostrDataSource<User>("SingleUserFeed") {
invalidateFilters() invalidateFilters()
} }
override fun feed(): List<User> {
return synchronized(usersToWatch) {
usersToWatch.map {
LocalCache.users[it]
}.filterNotNull()
}
}
override fun updateChannelFilters() { override fun updateChannelFilters() {
userChannel.typedFilters = listOfNotNull(createUserFilter(), createUserReportFilter()).flatten() userChannel.typedFilters = listOfNotNull(createUserFilter(), createUserReportFilter()).flatten()
} }
@@ -2,117 +2,48 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note 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.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
object NostrThreadDataSource: NostrDataSource<Note>("SingleThreadFeed") { object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") {
private var eventsToWatch = setOf<String>() private var eventToWatch: String? = null
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())
)
)
}
fun createLoadEventsIfNotLoadedFilter(): TypedFilter? { 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 } .filter { it.event == null }
.map { it.idHex.substring(0, 8) } .map { it.idHex }
.toSet()
if (eventsToLoad.isEmpty()) { .ifEmpty { null } ?: return null
return null
}
return TypedFilter( return TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
filter = JsonFilter( filter = JsonFilter(
ids = eventsToLoad ids = eventsToLoad.map { it.substring(0, 8) }
) )
) )
} }
val loadEventsChannel = requestNewChannel() val loadEventsChannel = requestNewChannel(){
// Many relays operate with limits in the amount of filters.
override fun feed(): List<Note> { // As information comes, the filters will be rotated to get more data.
// Currently orders by date of each event, descending, at each level of the reply stack
val order = compareByDescending<Note> { 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<Note> = 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<Note>()
val threadSet = mutableSetOf<Note>()
val threadRoot = searchRoot(note) ?: note
loadDown(threadRoot, thread, threadSet)
eventsToWatch = thread.map { it.idHex }.toSet()
} else {
eventsToWatch = setOf(noteId)
}
invalidateFilters() invalidateFilters()
} }
fun loadDown(note: Note, thread: MutableList<Note>, threadSet: MutableSet<Note>) { override fun updateChannelFilters() {
if (note !in threadSet) { loadEventsChannel.typedFilters = listOfNotNull(createLoadEventsIfNotLoadedFilter()).ifEmpty { null }
thread.add(note) }
threadSet.add(note)
note.replies.forEach { fun loadThread(noteId: String?) {
loadDown(it, thread, threadSet) eventToWatch = noteId
}
} invalidateFilters()
} }
} }
@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
@@ -12,14 +11,17 @@ import nostr.postr.events.ContactListEvent
import nostr.postr.events.MetadataEvent import nostr.postr.events.MetadataEvent
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
object NostrUserProfileDataSource: NostrDataSource<Note>("UserProfileFeed") { object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
lateinit var account: Account
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String) { fun loadUserProfile(userId: String?) {
if (userId != null) {
user = LocalCache.getOrCreateUser(userId) user = LocalCache.getOrCreateUser(userId)
} }
resetFilters()
}
fun createUserInfoFilter(): TypedFilter { fun createUserInfoFilter(): TypedFilter {
return TypedFilter( return TypedFilter(
types = FeedType.values().toSet(), types = FeedType.values().toSet(),
@@ -73,14 +75,6 @@ object NostrUserProfileDataSource: NostrDataSource<Note>("UserProfileFeed") {
val userInfoChannel = requestNewChannel() val userInfoChannel = requestNewChannel()
override fun feed(): List<Note> {
return user?.notes
?.filter { account.isAcceptable(it) }
?.sortedBy { it.event?.createdAt }
?.reversed()
?: emptyList()
}
override fun updateChannelFilters() { override fun updateChannelFilters() {
userInfoChannel.typedFilters = listOf( userInfoChannel.typedFilters = listOf(
createUserInfoFilter(), createUserInfoFilter(),
@@ -18,6 +18,7 @@ import coil.decode.SvgDecoder
import coil.disk.DiskCache import coil.disk.DiskCache
import coil.memory.MemoryCache import coil.memory.MemoryCache
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.util.DebugLogger
import com.vitorpamplona.amethyst.EncryptedStorage import com.vitorpamplona.amethyst.EncryptedStorage
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.ServiceManager
@@ -51,7 +52,7 @@ class MainActivity : ComponentActivity() {
add(GifDecoder.Factory()) add(GifDecoder.Factory())
} }
add(SvgDecoder.Factory()) add(SvgDecoder.Factory())
} } //.logger(DebugLogger())
.respectCacheHeaders(false) .respectCacheHeaders(false)
.build() .build()
} }
@@ -76,7 +77,7 @@ class MainActivity : ComponentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Only starts after login // Only starts after login
//ServiceManager.start() ServiceManager.start()
} }
override fun onPause() { override fun onPause() {
@@ -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<Note>() {
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<Note> {
return channel?.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList()
}
}
@@ -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<Note>() {
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<Note> {
val messages = account.userProfile().privateChatrooms[withUser] ?: return emptyList()
return messages.roomMessages.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed()
}
}
@@ -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<Note>() {
lateinit var account: Account
// returns the last Note of each user.
override fun feed(): List<Note> {
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()
}
}
@@ -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<Note>() {
lateinit var account: Account
// returns the last Note of each user.
override fun feed(): List<Note> {
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()
}
}
@@ -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<T>() {
fun loadTop(): List<T> {
return feed().take(1000)
}
abstract fun feed(): List<T>
}
@@ -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<Note>() {
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()
}
@@ -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.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
@@ -7,10 +7,8 @@ import com.vitorpamplona.amethyst.model.User
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
object NostrHiddenAccountsDataSource: NostrDataSource<User>("HiddenAccounts") { object HiddenAccountsFeedFilter: FeedFilter<User>() {
lateinit var account: Account lateinit var account: Account
override fun feed() = account.hiddenUsers() override fun feed() = account.hiddenUsers()
override fun updateChannelFilters() {}
} }
@@ -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<Note>() {
lateinit var account: Account
override fun feed(): List<Note> {
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()
}
}
@@ -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<Note>() {
lateinit var account: Account
override fun feed(): List<Note> {
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()
}
}
@@ -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.Account
import com.vitorpamplona.amethyst.model.Note 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 com.vitorpamplona.amethyst.service.model.RepostEvent
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
object NostrNotificationDataSource: NostrDataSource<Note>("NotificationFeed") { object NotificationFeedFilter: FeedFilter<Note>() {
lateinit var account: Account lateinit var account: Account
override fun feed(): List<Note> { override fun feed(): List<Note> {
@@ -25,6 +25,4 @@ object NostrNotificationDataSource: NostrDataSource<Note>("NotificationFeed") {
.sortedBy { it.event?.createdAt } .sortedBy { it.event?.createdAt }
.reversed() .reversed()
} }
override fun updateChannelFilters() {}
} }
@@ -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<Note>() {
var noteId: String? = null
override fun feed(): List<Note> {
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<Note> { it.replyLevelSignature() }
return eventsToWatch.sortedWith(order)
}
fun loadThread(noteId: String?) {
this.noteId = noteId
}
}
@@ -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.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent
object NostrUserProfileFollowersDataSource: NostrDataSource<User>("UserProfileFollowerFeed") { object UserProfileFollowersFeedFilter: FeedFilter<User>() {
lateinit var account: Account lateinit var account: Account
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String) { fun loadUserProfile(accountLoggedIn: Account, userId: String) {
account = accountLoggedIn
user = LocalCache.users[userId] user = LocalCache.users[userId]
resetFilters()
} }
override fun feed(): List<User> { override fun feed(): List<User> {
return user?.followers?.filter { account.isAcceptable(it) } ?: emptyList() return user?.followers?.filter { account.isAcceptable(it) } ?: emptyList()
} }
override fun updateChannelFilters() {}
} }
@@ -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.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent
object NostrUserProfileFollowsDataSource: NostrDataSource<User>("UserProfileFollowsFeed") { object UserProfileFollowsFeedFilter: FeedFilter<User>() {
lateinit var account: Account lateinit var account: Account
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String) { fun loadUserProfile(accountLoggedIn: Account, userId: String) {
account = accountLoggedIn
user = LocalCache.users[userId] user = LocalCache.users[userId]
resetFilters()
} }
override fun feed(): List<User> { override fun feed(): List<User> {
return user?.follows?.filter { account.isAcceptable(it) } ?: emptyList() return user?.follows?.filter { account.isAcceptable(it) } ?: emptyList()
} }
override fun updateChannelFilters() {}
} }
@@ -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<Note>() {
lateinit var account: Account
var user: User? = null
fun loadUserProfile(accountLoggedIn: Account, userId: String) {
account = accountLoggedIn
user = LocalCache.getOrCreateUser(userId)
}
override fun feed(): List<Note> {
return user?.notes
?.filter { account.isAcceptable(it) }
?.sortedBy { it.event?.createdAt }
?.reversed()
?: emptyList()
}
}
@@ -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.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent
import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent
object NostrUserProfileZapsDataSource: NostrDataSource<Pair<Note,Note>>("UserProfileZapsFeed") { object UserProfileZapsFeedFilter: FeedFilter<Pair<Note,Note>>() {
lateinit var account: Account
var user: User? = null var user: User? = null
fun loadUserProfile(userId: String) { fun loadUserProfile(userId: String) {
user = LocalCache.users[userId] user = LocalCache.getOrCreateUser(userId)
resetFilters()
} }
override fun feed(): List<Pair<Note,Note>> { override fun feed(): List<Pair<Note,Note>> {
return (user?.zaps?.filter { it.value != null }?.toList()?.sortedBy { (it.second?.event as? LnZapEvent)?.amount }?.reversed() ?: emptyList()) as List<Pair<Note, Note>> return (user?.zaps?.filter { it.value != null }?.toList()?.sortedBy { (it.second?.event as? LnZapEvent)?.amount }?.reversed() ?: emptyList()) as List<Pair<Note, Note>>
} }
override fun updateChannelFilters() {}
} }
@@ -4,7 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
@@ -16,12 +15,18 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState 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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -29,7 +34,7 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.model.LocalCache 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 import kotlinx.coroutines.launch
val bottomNavigationItems = listOf( val bottomNavigationItems = listOf(
@@ -40,7 +45,7 @@ val bottomNavigationItems = listOf(
) )
@Composable @Composable
fun AppBottomBar(navController: NavHostController) { fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) {
val currentRoute = currentRoute(navController) val currentRoute = currentRoute(navController)
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -55,7 +60,7 @@ fun AppBottomBar(navController: NavHostController) {
) { ) {
bottomNavigationItems.forEach { item -> bottomNavigationItems.forEach { item ->
BottomNavigationItem( BottomNavigationItem(
icon = { NotifiableIcon(item, currentRoute) }, icon = { NotifiableIcon(item, currentRoute, accountViewModel) },
selected = currentRoute == item.route, selected = currentRoute == item.route,
onClick = { onClick = {
coroutineScope.launch { coroutineScope.launch {
@@ -89,7 +94,7 @@ fun AppBottomBar(navController: NavHostController) {
} }
@Composable @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)) { Box(Modifier.size(if ("Home" == item.route) 25.dp else 23.dp)) {
Icon( Icon(
painter = painterResource(id = item.icon), painter = painterResource(id = item.icon),
@@ -98,15 +103,25 @@ private fun NotifiableIcon(item: Route, currentRoute: String?) {
tint = if (currentRoute == item.route) MaterialTheme.colors.primary else Color.Unspecified tint = if (currentRoute == item.route) MaterialTheme.colors.primary else Color.Unspecified
) )
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
// Notification // Notification
val dbState = LocalCache.live.observeAsState() val dbState = LocalCache.live.observeAsState()
val db = dbState.value ?: return
val notifState = NotificationCache.live.observeAsState() val notifState = NotificationCache.live.observeAsState()
val notif = notifState.value ?: return
val db = dbState.value var hasNewItems by remember { mutableStateOf<Boolean>(false) }
val notif = notifState.value
if (db != null && notif != null) { val context = LocalContext.current.applicationContext
if (item.hasNewItems(db.cache, notif.cache)) {
LaunchedEffect(key1 = notif) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
}
if (hasNewItems) {
Box( Box(
Modifier Modifier
.width(10.dp) .width(10.dp)
@@ -134,5 +149,4 @@ private fun NotifiableIcon(item: Route, currentRoute: String?) {
} }
} }
} }
}
} }
@@ -1,7 +1,6 @@
package com.vitorpamplona.amethyst.ui.navigation package com.vitorpamplona.amethyst.ui.navigation
import android.util.Log import android.util.Log
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.Color
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource 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.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil.Coil import coil.Coil
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource 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.NostrChatroomListDataSource
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource 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.Client
import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView 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.RelayPoolViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import java.net.URLEncoder
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@@ -141,12 +128,11 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
NostrAccountDataSource.printCounter() NostrAccountDataSource.printCounter()
NostrChannelDataSource.printCounter() NostrChannelDataSource.printCounter()
NostrChatRoomDataSource.printCounter() NostrChatroomDataSource.printCounter()
NostrChatroomListDataSource.printCounter() NostrChatroomListDataSource.printCounter()
NostrGlobalDataSource.printCounter() NostrGlobalDataSource.printCounter()
NostrHomeDataSource.printCounter() NostrHomeDataSource.printCounter()
NostrNotificationDataSource.printCounter()
NostrSingleEventDataSource.printCounter() NostrSingleEventDataSource.printCounter()
NostrSearchEventOrUserDataSource.printCounter() NostrSearchEventOrUserDataSource.printCounter()
@@ -155,9 +141,6 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
NostrThreadDataSource.printCounter() NostrThreadDataSource.printCounter()
NostrUserProfileDataSource.printCounter() NostrUserProfileDataSource.printCounter()
NostrUserProfileFollowersDataSource.printCounter()
NostrUserProfileFollowsDataSource.printCounter()
NostrUserProfileZapsDataSource.printCounter()
println("Connected Relays: " + RelayPool.connectedRelays()) println("Connected Relays: " + RelayPool.connectedRelays())
@@ -1,7 +1,9 @@
package com.vitorpamplona.amethyst.ui.navigation package com.vitorpamplona.amethyst.ui.navigation
import android.content.Context
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NamedNavArgument import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
@@ -12,21 +14,16 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.service.NostrDataSource import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrNotificationDataSource
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.ChannelScreen import com.vitorpamplona.amethyst.ui.screen.ChannelScreen
import com.vitorpamplona.amethyst.ui.screen.ChatroomListScreen import com.vitorpamplona.amethyst.ui.screen.ChatroomListScreen
import com.vitorpamplona.amethyst.ui.screen.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.ChatroomScreen
import com.vitorpamplona.amethyst.ui.screen.FiltersScreen import com.vitorpamplona.amethyst.ui.screen.FiltersScreen
import com.vitorpamplona.amethyst.ui.screen.HomeScreen 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.NotificationScreen
import com.vitorpamplona.amethyst.ui.screen.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.ProfileScreen
import com.vitorpamplona.amethyst.ui.screen.SearchScreen import com.vitorpamplona.amethyst.ui.screen.SearchScreen
@@ -37,24 +34,24 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
sealed class Route( sealed class Route(
val route: String, val route: String,
val icon: Int, val icon: Int,
val hasNewItems: @Composable (LocalCache, NotificationCache) -> Boolean = @Composable { _,_ -> false }, val hasNewItems: (Account, NotificationCache, Context) -> Boolean = { _,_,_ -> false },
val arguments: List<NamedNavArgument> = emptyList(), val arguments: List<NamedNavArgument> = emptyList(),
val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit
) { ) {
object Home : Route("Home", R.drawable.ic_home, 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) } } buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } }
) )
object Search : Route("Search", R.drawable.ic_search, object Search : Route("Search", R.drawable.ic_search,
buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) }} buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) }}
) )
object Notification : Route("Notification", R.drawable.ic_notifications, 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) }} buildScreen = { acc, accSt, nav -> { _ -> NotificationScreen(acc, nav) }}
) )
object Message : Route("Message", R.drawable.ic_dm, 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) }} buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }}
) )
@@ -107,45 +104,32 @@ public fun currentRoute(navController: NavHostController): String? {
return navBackStackEntry?.destination?.route return navBackStackEntry?.destination?.route
} }
@Composable private fun homeHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean {
private fun homeHasNewItems(cache: NotificationCache): Boolean { val lastTime = cache.load("HomeFollows", context)
val context = LocalContext.current.applicationContext
val lastTimeFollows = cache.load("HomeFollows", context)
val homeFeed = NostrHomeDataSource.feed().take(100) HomeNewThreadFeedFilter.account = account
val hasNewInFollows = homeFeed.filter { return HomeNewThreadFeedFilter.feed().any {(it.event?.createdAt ?: 0) > lastTime }
it.isNewThread()
}.filter {
(it.event?.createdAt ?: 0) > lastTimeFollows
}.isNotEmpty()
return hasNewInFollows
} }
@Composable private fun notificationHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean {
private fun notificationHasNewItems(cache: NotificationCache): Boolean {
val context = LocalContext.current.applicationContext
val lastTime = cache.load("Notification", context) val lastTime = cache.load("Notification", context)
return NostrNotificationDataSource.loadTop()
.filter { it.event != null && it.event!!.createdAt > lastTime } NotificationFeedFilter.account = account
.isNotEmpty()
return NotificationFeedFilter.feed().any {(it.event?.createdAt ?: 0) > lastTime }
} }
@Composable private fun messagesHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean {
private fun messagesHasNewItems(cache: NotificationCache): Boolean { ChatroomListKnownFeedFilter.account = account
val context = LocalContext.current.applicationContext
return NostrChatroomListDataSource.feed().take(100).filter { return ChatroomListKnownFeedFilter.feed().any {
// only for known sources if (it.channel == null) {
val me = NostrChatroomListDataSource.account.userProfile() val lastTime = cache.load("Room/${it.author?.pubkeyHex}", context)
it.channel == null && me.hasSentMessagesTo(it.author) && it.author != me
}.filter { (it.event?.createdAt ?: 0) > lastTime
val lastTime = if (it.channel != null) {
cache.load("Channel/${it.channel!!.idHex}", context)
} else { } else {
cache.load("Room/${it.author?.pubkeyHex}", context) false
}
} }
NostrChatroomListDataSource.account.isAcceptable(it) && it.event != null && it.event!!.createdAt > lastTime
}.isNotEmpty()
} }
@@ -1,30 +1,28 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
@@ -32,8 +30,8 @@ import com.vitorpamplona.amethyst.ui.screen.BoostSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @Composable
fun BoostSetCompose(likeSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by likeSetCard.note.live.observeAsState() val noteState by boostSetCard.note.live.observeAsState()
val note = noteState?.note val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -44,8 +42,15 @@ fun BoostSetCompose(likeSetCard: BoostSetCard, isInnerNote: Boolean = false, rou
if (note?.event == null) { if (note?.event == null) {
BlankNote(Modifier, isInnerNote) BlankNote(Modifier, isInnerNote)
} else { } else {
val isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context) var isNew by remember { mutableStateOf<Boolean>(false) }
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context)
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( Column(
modifier = Modifier.background( 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)) { Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
FlowRow() { FlowRow() {
likeSetCard.boostEvents.forEach { boostSetCard.boostEvents.forEach {
NoteAuthorPicture( NoteAuthorPicture(
note = it, note = it,
navController = navController, navController = navController,
@@ -20,8 +20,12 @@ import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -81,11 +85,13 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
noteEvent?.content noteEvent?.content
} }
channel?.let { channel -> channel?.let { channel ->
val hasNewMessages = var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
if (noteEvent != null)
noteEvent.createdAt > notificationCache.cache.load("Channel/${channel.idHex}", context) LaunchedEffect(key1 = notificationCache) {
else noteEvent?.let {
false hasNewMessages = it.createdAt > notificationCache.cache.load("Channel/${channel.idHex}", context)
}
}
ChannelName( ChannelName(
channelPicture = channel.profilePicture(), channelPicture = channel.profilePicture(),
@@ -122,11 +128,13 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
val noteEvent = note.event val noteEvent = note.event
userToComposeOn.let { user -> userToComposeOn.let { user ->
val hasNewMessages = var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
if (noteEvent != null)
noteEvent.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context) LaunchedEffect(key1 = notificationCache) {
else noteEvent?.let {
false hasNewMessages = it.createdAt > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context)
}
}
ChannelName( ChannelName(
channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) }, channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) },
@@ -30,6 +30,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -115,15 +116,18 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
shape = ChatBubbleShapeThem shape = ChatBubbleShapeThem
} }
// Mark read var isNew by remember { mutableStateOf<Boolean>(false) }
val isNew = routeForLastRead?.run {
val isNew = NotificationCache.load(this, context) LaunchedEffect(key1 = routeForLastRead) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it, context)
val createdAt = note.event?.createdAt val createdAt = note.event?.createdAt
if (createdAt != null) if (createdAt != null) {
NotificationCache.markAsRead(this, createdAt, context) NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
isNew }
}
} }
Column() { Column() {
@@ -13,8 +13,12 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -44,8 +48,15 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn
if (note == null) { if (note == null) {
BlankNote(Modifier, isInnerNote) BlankNote(Modifier, isInnerNote)
} else { } else {
val isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context) var isNew by remember { mutableStateOf<Boolean>(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) NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context)
}
Column( Column(
modifier = Modifier.background( modifier = Modifier.background(
@@ -31,6 +31,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -120,17 +121,19 @@ fun NoteCompose(
onClick = { showHiddenNote = true } onClick = { showHiddenNote = true }
) )
} else { } else {
val isNew = routeForLastRead?.run { var isNew by remember { mutableStateOf<Boolean>(false) }
val lastTime = NotificationCache.load(this, context)
LaunchedEffect(key1 = routeForLastRead) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it, context)
val createdAt = note.event?.createdAt val createdAt = note.event?.createdAt
if (createdAt != null) { if (createdAt != null) {
NotificationCache.markAsRead(this, createdAt, context) NotificationCache.markAsRead(it, createdAt, context)
createdAt > lastTime isNew = createdAt > lastTime
} else { }
false }
} }
} ?: false
Column(modifier = Column(modifier =
modifier.combinedClickable( modifier.combinedClickable(
@@ -12,8 +12,12 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Bolt
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -42,8 +46,15 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, modifier: Modifier = Modifier, isInner
if (note == null) { if (note == null) {
BlankNote(Modifier, isInnerNote) BlankNote(Modifier, isInnerNote)
} else { } else {
val isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead, context) var isNew by remember { mutableStateOf<Boolean>(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) NotificationCache.markAsRead(routeForLastRead, zapSetCard.createdAt, context)
}
Column( Column(
modifier = Modifier.background( modifier = Modifier.background(
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -8,7 +9,9 @@ import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -22,11 +25,13 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences): Vie
init { init {
// pulls account from storage. // pulls account from storage.
viewModelScope.launch(Dispatchers.IO) {
// Keeps it in the the UI thread to void blinking the login page.
//viewModelScope.launch(Dispatchers.IO) {
localPreferences.loadFromEncryptedStorage()?.let { localPreferences.loadFromEncryptedStorage()?.let {
login(it) login(it)
} }
} //}
} }
fun login(key: String) { fun login(key: String) {
@@ -61,7 +66,8 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences): Vie
else else
_accountContent.update { AccountState.LoggedInViewOnly ( account ) } _accountContent.update { AccountState.LoggedInViewOnly ( account ) }
viewModelScope.launch(Dispatchers.IO) { val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
ServiceManager.start(account) ServiceManager.start(account)
} }
} }
@@ -1,32 +1,30 @@
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import android.os.Handler
import android.os.Looper
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note 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.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent 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 java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() { object NotificationViewModel: CardFeedViewModel(NotificationFeedFilter)
open class CardFeedViewModel(val dataSource: FeedFilter<Note>): ViewModel() {
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading) private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
val feedContent = _feedContent.asStateFlow() val feedContent = _feedContent.asStateFlow()
@@ -125,8 +123,7 @@ class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
handlerWaiting.set(true) handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default) val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch { scope.launch {
if (feedContent.value is CardFeedState.Loaded) delay(50)
delay(5000)
refresh() refresh()
handlerWaiting.set(false) handlerWaiting.set(false)
} }
@@ -36,7 +36,12 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @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() val feedState by viewModel.feedContent.collectAsState()
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
@@ -44,7 +49,7 @@ fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navCo
LaunchedEffect(isRefreshing) { LaunchedEffect(isRefreshing) {
if (isRefreshing) { if (isRefreshing) {
viewModel.hardRefresh() viewModel.refresh()
isRefreshing = false isRefreshing = false
} }
} }
@@ -2,18 +2,19 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter
import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter
import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
import com.vitorpamplona.amethyst.service.NostrDataSource import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter
import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource 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 java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -23,61 +24,28 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class NostrChannelFeedViewModel: FeedViewModel(NostrChannelDataSource) class NostrChannelFeedViewModel: FeedViewModel(ChannelFeedFilter)
class NostrChatRoomFeedViewModel: FeedViewModel(NostrChatRoomDataSource) class NostrChatRoomFeedViewModel: FeedViewModel(ChatroomFeedFilter)
class NostrGlobalFeedViewModel: FeedViewModel(NostrGlobalDataSource) class NostrGlobalFeedViewModel: FeedViewModel(GlobalFeedFilter)
class NostrThreadFeedViewModel: FeedViewModel(NostrThreadDataSource) class NostrThreadFeedViewModel: FeedViewModel(ThreadFeedFilter)
class NostrUserProfileFeedViewModel: FeedViewModel(NostrUserProfileDataSource) class NostrUserProfileFeedViewModel: FeedViewModel(UserProfileNoteFeedFilter)
class NostrChatroomListKnownFeedViewModel: FeedViewModel(ChatroomListKnownFeedFilter)
class NostrChatroomListKnownFeedViewModel: FeedViewModel(NostrChatroomListDataSource) { class NostrChatroomListNewFeedViewModel: FeedViewModel(ChatroomListNewFeedFilter)
override fun newListFromDataSource(): List<Note> { class NostrHomeFeedViewModel: FeedViewModel(HomeNewThreadFeedFilter)
// Filter: all channels + PMs the account has replied to class NostrHomeRepliesFeedViewModel: FeedViewModel(HomeConversationsFeedFilter)
return super.newListFromDataSource().filter {
val me = NostrChatroomListDataSource.account.userProfile()
it.channel != null || me.hasSentMessagesTo(it.author)
}
}
}
class NostrChatroomListNewFeedViewModel: FeedViewModel(NostrChatroomListDataSource) {
override fun newListFromDataSource(): List<Note> {
// 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<Note> {
// Filter: no replies
return dataSource.feed().filter { it.isNewThread() }.take(1000)
}
}
class NostrHomeRepliesFeedViewModel: FeedViewModel(NostrHomeDataSource) {
override fun newListFromDataSource(): List<Note> {
// Filter: only replies
return dataSource.feed().filter {! it.isNewThread() }.take(1000)
}
}
abstract class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() { abstract class FeedViewModel(val localFilter: FeedFilter<Note>): ViewModel() {
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading) private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
val feedContent = _feedContent.asStateFlow() val feedContent = _feedContent.asStateFlow()
open fun newListFromDataSource(): List<Note> { open fun newListFromDataSource(): List<Note> {
return dataSource.loadTop() return localFilter.loadTop()
}
fun hardRefresh() {
dataSource.resetFilters()
} }
fun refresh() { fun refresh() {
println("Model Refresh: ${this::class.simpleName}")
val scope = CoroutineScope(Job() + Dispatchers.Default) val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch { scope.launch {
refreshSuspended() refreshSuspended()
@@ -90,7 +58,7 @@ abstract class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel()
val oldNotesState = feedContent.value val oldNotesState = feedContent.value
if (oldNotesState is FeedState.Loaded) { if (oldNotesState is FeedState.Loaded) {
// Using size as a proxy for has changed. // 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) updateFeed(notes)
} }
} else { } else {
@@ -116,14 +84,13 @@ abstract class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel()
private var handlerWaiting = AtomicBoolean() private var handlerWaiting = AtomicBoolean()
@Synchronized @Synchronized
private fun invalidateData() { fun invalidateData() {
if (handlerWaiting.getAndSet(true)) return if (handlerWaiting.getAndSet(true)) return
handlerWaiting.set(true) handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default) val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch { scope.launch {
if (feedContent.value is FeedState.Loaded) delay(50)
delay(5000)
refresh() refresh()
handlerWaiting.set(false) handlerWaiting.set(false)
} }
@@ -2,16 +2,14 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.NostrDataSource import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.service.NostrUserProfileZapsDataSource import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -20,9 +18,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class NostrUserProfileZapsFeedViewModel: LnZapFeedViewModel(NostrUserProfileZapsDataSource) class NostrUserProfileZapsFeedViewModel: LnZapFeedViewModel(UserProfileZapsFeedFilter)
open class LnZapFeedViewModel(val dataSource: NostrDataSource<Pair<Note, Note>>): ViewModel() { open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>): ViewModel() {
private val _feedContent = MutableStateFlow<LnZapFeedState>(LnZapFeedState.Loading) private val _feedContent = MutableStateFlow<LnZapFeedState>(LnZapFeedState.Loading)
val feedContent = _feedContent.asStateFlow() val feedContent = _feedContent.asStateFlow()
@@ -33,14 +31,13 @@ open class LnZapFeedViewModel(val dataSource: NostrDataSource<Pair<Note, Note>>)
} }
} }
private fun refreshSuspended() { private fun refreshSuspended() {
val notes = dataSource.loadTop() val notes = dataSource.loadTop()
val oldNotesState = feedContent.value val oldNotesState = feedContent.value
if (oldNotesState is LnZapFeedState.Loaded) { if (oldNotesState is LnZapFeedState.Loaded) {
// Using size as a proxy for has changed. // 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) updateFeed(notes)
} }
} else { } else {
@@ -72,7 +69,7 @@ open class LnZapFeedViewModel(val dataSource: NostrDataSource<Pair<Note, Note>>)
handlerWaiting.set(true) handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default) val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch { scope.launch {
delay(1000) delay(50)
refresh() refresh()
handlerWaiting.set(false) handlerWaiting.set(false)
} }
@@ -1,43 +1,29 @@
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import android.os.Handler
import android.os.Looper
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrDataSource import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.service.NostrHiddenAccountsDataSource import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class NostrUserProfileFollowsUserFeedViewModel(): UserFeedViewModel( class NostrUserProfileFollowsUserFeedViewModel: UserFeedViewModel(UserProfileFollowsFeedFilter)
NostrUserProfileFollowsDataSource class NostrUserProfileFollowersUserFeedViewModel: UserFeedViewModel(UserProfileFollowersFeedFilter)
) class NostrHiddenAccountsFeedViewModel: UserFeedViewModel(HiddenAccountsFeedFilter)
class NostrUserProfileFollowersUserFeedViewModel(): UserFeedViewModel( open class UserFeedViewModel(val dataSource: FeedFilter<User>): ViewModel() {
NostrUserProfileFollowersDataSource
)
class NostrHiddenAccountsFeedViewModel(): UserFeedViewModel(
NostrHiddenAccountsDataSource
)
open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel() {
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading) private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
val feedContent = _feedContent.asStateFlow() val feedContent = _feedContent.asStateFlow()
@@ -48,14 +34,13 @@ open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel()
} }
} }
private fun refreshSuspended() { private fun refreshSuspended() {
val notes = dataSource.loadTop() val notes = dataSource.loadTop()
val oldNotesState = feedContent.value val oldNotesState = feedContent.value
if (oldNotesState is UserFeedState.Loaded) { if (oldNotesState is UserFeedState.Loaded) {
// Using size as a proxy for has changed. // 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) updateFeed(notes)
} }
} else { } else {
@@ -87,8 +72,7 @@ open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel()
handlerWaiting.set(true) handlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default) val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch { scope.launch {
if (feedContent.value is UserFeedState.Loaded) delay(50)
delay(5000)
refresh() refresh()
handlerWaiting.set(false) handlerWaiting.set(false)
} }
@@ -28,6 +28,7 @@ import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@@ -42,6 +43,7 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage 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.Note
import com.vitorpamplona.amethyst.model.toNote import com.vitorpamplona.amethyst.model.toNote
import com.vitorpamplona.amethyst.service.NostrChannelDataSource 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.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.actions.NewChannelView import com.vitorpamplona.amethyst.ui.actions.NewChannelView
import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.PostButton 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.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import nostr.postr.toNpub import nostr.postr.toNpub
@@ -79,15 +85,36 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
if (account != null && channelId != null) { if (account != null && channelId != null) {
val newPost = remember { mutableStateOf(TextFieldValue("")) } val newPost = remember { mutableStateOf(TextFieldValue("")) }
ChannelFeedFilter.loadMessagesBetween(account, channelId)
NostrChannelDataSource.loadMessagesBetween(channelId) NostrChannelDataSource.loadMessagesBetween(channelId)
val channelState by NostrChannelDataSource.channel!!.live.observeAsState() val channelState by NostrChannelDataSource.channel!!.live.observeAsState()
val channel = channelState?.channel ?: return val channel = channelState?.channel ?: return
val feedViewModel: NostrChannelFeedViewModel = viewModel() val feedViewModel: NostrChannelFeedViewModel = viewModel()
val lifeCycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) { 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()) { Column(Modifier.fillMaxHeight()) {
@@ -9,12 +9,16 @@ import androidx.compose.material.TabRow
import androidx.compose.material.TabRowDefaults import androidx.compose.material.TabRowDefaults
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.google.accompanist.pager.ExperimentalPagerApi 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.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.vitorpamplona.amethyst.model.User 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 com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -72,11 +80,35 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon
@Composable @Composable
fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
ChatroomListKnownFeedFilter.account = account
val feedViewModel: NostrChatroomListKnownFeedViewModel = viewModel() val feedViewModel: NostrChatroomListKnownFeedViewModel = viewModel()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
feedViewModel.hardRefresh() // refresh filters NostrChatroomListDataSource.resetFilters()
feedViewModel.refresh() // refresh view 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()) { Column(Modifier.fillMaxHeight()) {
@@ -90,13 +122,37 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) {
@Composable @Composable
fun TabNew(accountViewModel: AccountViewModel, navController: NavController) { fun TabNew(accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
ChatroomListNewFeedFilter.account = account
val feedViewModel: NostrChatroomListNewFeedViewModel = viewModel() val feedViewModel: NostrChatroomListNewFeedViewModel = viewModel()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
feedViewModel.hardRefresh() // refresh filters NostrChatroomListDataSource.resetFilters()
feedViewModel.refresh() // refresh view 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.fillMaxHeight()) {
Column( Column(
modifier = Modifier.padding(vertical = 0.dp) modifier = Modifier.padding(vertical = 0.dp)
@@ -19,35 +19,36 @@ import androidx.compose.material.Text
import androidx.compose.material.TextField import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext 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.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp 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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.User 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.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.actions.PostButton 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.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -59,16 +60,37 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
if (account != null && userId != null) { if (account != null && userId != null) {
val newPost = remember { mutableStateOf(TextFieldValue("")) } val newPost = remember { mutableStateOf(TextFieldValue("")) }
NostrChatRoomDataSource.loadMessagesBetween(account, userId) ChatroomFeedFilter.loadMessagesBetween(account, userId)
NostrChatroomDataSource.loadMessagesBetween(account, userId)
val feedViewModel: NostrChatRoomFeedViewModel = viewModel() val feedViewModel: NostrChatRoomFeedViewModel = viewModel()
val lifeCycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) { 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()) { Column(Modifier.fillMaxHeight()) {
NostrChatRoomDataSource.withUser?.let { NostrChatroomDataSource.withUser?.let {
ChatroomHeader( ChatroomHeader(
it, it,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -9,7 +9,6 @@ import androidx.compose.material.TabRow
import androidx.compose.material.TabRowDefaults import androidx.compose.material.TabRowDefaults
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope 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.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.vitorpamplona.amethyst.service.NostrHiddenAccountsDataSource import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -33,7 +31,7 @@ fun FiltersScreen(accountViewModel: AccountViewModel, navController: NavControll
val account = accountState?.account val account = accountState?.account
if (account != null) { if (account != null) {
NostrHiddenAccountsDataSource.account = account HiddenAccountsFeedFilter.account = account
val feedViewModel: NostrHiddenAccountsFeedViewModel = viewModel() val feedViewModel: NostrHiddenAccountsFeedViewModel = viewModel()
@@ -9,16 +9,26 @@ import androidx.compose.material.TabRow
import androidx.compose.material.TabRowDefaults import androidx.compose.material.TabRowDefaults
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState 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.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -26,6 +36,12 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) { 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 feedViewModel: NostrHomeFeedViewModel = viewModel()
val feedViewModelReplies: NostrHomeRepliesFeedViewModel = viewModel() val feedViewModelReplies: NostrHomeRepliesFeedViewModel = viewModel()
@@ -33,8 +49,31 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController)
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
feedViewModel.refresh() NostrHomeDataSource.resetFilters()
feedViewModelReplies.refresh()
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()) { Column(Modifier.fillMaxHeight()) {
@@ -38,7 +38,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
.background(MaterialTheme.colors.primaryVariant) .background(MaterialTheme.colors.primaryVariant)
.statusBarsPadding(), .statusBarsPadding(),
bottomBar = { bottomBar = {
AppBottomBar(navController) AppBottomBar(navController, accountViewModel)
}, },
topBar = { topBar = {
AppTopBar(navController, scaffoldState, accountViewModel) AppTopBar(navController, scaffoldState, accountViewModel)
@@ -11,13 +11,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController 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.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @Composable
fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavController) { 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) { LaunchedEffect(Unit) {
feedViewModel.refresh() feedViewModel.refresh()
@@ -1,11 +1,8 @@
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText 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.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource 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.service.model.ReportEvent
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest 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.UserPicture
import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -81,10 +75,12 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
if (userId == null) return if (userId == null) return
UserProfileNoteFeedFilter.loadUserProfile(account, userId)
UserProfileFollowersFeedFilter.loadUserProfile(account, userId)
UserProfileFollowsFeedFilter.loadUserProfile(account, userId)
UserProfileZapsFeedFilter.loadUserProfile(userId)
NostrUserProfileDataSource.loadUserProfile(userId) NostrUserProfileDataSource.loadUserProfile(userId)
NostrUserProfileFollowersDataSource.loadUserProfile(userId)
NostrUserProfileFollowsDataSource.loadUserProfile(userId)
NostrUserProfileZapsDataSource.loadUserProfile(userId)
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
@@ -92,17 +88,13 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
val observer = LifecycleEventObserver { source, event -> val observer = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
println("Profile Start") println("Profile Start")
NostrUserProfileDataSource.loadUserProfile(userId)
NostrUserProfileDataSource.start() NostrUserProfileDataSource.start()
NostrUserProfileFollowersDataSource.start()
NostrUserProfileFollowsDataSource.start()
NostrUserProfileZapsDataSource.start()
} }
if (event == Lifecycle.Event.ON_PAUSE) { if (event == Lifecycle.Event.ON_PAUSE) {
println("Profile Stop") println("Profile Stop")
NostrUserProfileDataSource.loadUserProfile(null)
NostrUserProfileDataSource.stop() NostrUserProfileDataSource.stop()
NostrUserProfileFollowsDataSource.stop()
NostrUserProfileFollowersDataSource.stop()
NostrUserProfileZapsDataSource.stop()
} }
} }
@@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource 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.ChannelName
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UserCompose
@@ -76,11 +77,16 @@ import kotlinx.coroutines.withContext
@Composable @Composable
fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController) { 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 feedViewModel: NostrGlobalFeedViewModel = viewModel()
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
feedViewModel.refresh() feedViewModel.invalidateData()
} }
DisposableEffect(accountViewModel) { DisposableEffect(accountViewModel) {
@@ -88,6 +94,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
println("Global Start") println("Global Start")
NostrGlobalDataSource.start() NostrGlobalDataSource.start()
feedViewModel.invalidateData()
} }
if (event == Lifecycle.Event.ON_PAUSE) { if (event == Lifecycle.Event.ON_PAUSE) {
println("Global Stop") println("Global Stop")
@@ -16,10 +16,7 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileZapsDataSource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable @Composable
@@ -28,14 +25,28 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, navControl
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
if (account != null && noteId != null) {
ThreadFeedFilter.loadThread(noteId)
NostrThreadDataSource.loadThread(noteId)
val feedViewModel: NostrThreadFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.invalidateData()
}
DisposableEffect(accountViewModel) { DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { source, event -> val observer = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
println("Thread Start") println("Thread Start")
ThreadFeedFilter.loadThread(noteId)
NostrThreadDataSource.loadThread(noteId)
NostrThreadDataSource.start() NostrThreadDataSource.start()
feedViewModel.invalidateData()
} }
if (event == Lifecycle.Event.ON_PAUSE) { if (event == Lifecycle.Event.ON_PAUSE) {
println("Thread Stop") println("Thread Stop")
ThreadFeedFilter.loadThread(null)
NostrThreadDataSource.loadThread(null)
NostrThreadDataSource.stop() NostrThreadDataSource.stop()
} }
} }
@@ -46,15 +57,6 @@ fun ThreadScreen(noteId: String?, accountViewModel: AccountViewModel, navControl
} }
} }
if (account != null && noteId != null) {
NostrThreadDataSource.loadThread(noteId)
val feedViewModel: NostrThreadFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
Column(Modifier.fillMaxHeight()) { Column(Modifier.fillMaxHeight()) {
Column( Column(
modifier = Modifier.padding(vertical = 0.dp) modifier = Modifier.padding(vertical = 0.dp)