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:
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-9
@@ -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 }
|
||||||
}
|
}
|
||||||
+1
-17
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -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()
|
||||||
|
|
||||||
|
}
|
||||||
+2
-4
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-4
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-8
@@ -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() {}
|
|
||||||
}
|
}
|
||||||
+4
-8
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-10
@@ -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()) {
|
||||||
|
|||||||
+59
-3
@@ -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)
|
||||||
|
|||||||
+6
-2
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user