mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2024-09-30 00:40:49 +00:00
Adds support for Private and Public Follow Lists
This commit is contained in:
parent
76283463b0
commit
8c2e89197e
@ -46,6 +46,8 @@ private object PrefKeys {
|
|||||||
const val ZAP_AMOUNTS = "zapAmounts"
|
const val ZAP_AMOUNTS = "zapAmounts"
|
||||||
const val DEFAULT_ZAPTYPE = "defaultZapType"
|
const val DEFAULT_ZAPTYPE = "defaultZapType"
|
||||||
const val DEFAULT_FILE_SERVER = "defaultFileServer"
|
const val DEFAULT_FILE_SERVER = "defaultFileServer"
|
||||||
|
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
|
||||||
|
const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList"
|
||||||
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
||||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||||
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
||||||
@ -197,6 +199,8 @@ object LocalPreferences {
|
|||||||
putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
|
putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
|
||||||
putString(PrefKeys.DEFAULT_ZAPTYPE, gson.toJson(account.defaultZapType))
|
putString(PrefKeys.DEFAULT_ZAPTYPE, gson.toJson(account.defaultZapType))
|
||||||
putString(PrefKeys.DEFAULT_FILE_SERVER, gson.toJson(account.defaultFileServer))
|
putString(PrefKeys.DEFAULT_FILE_SERVER, gson.toJson(account.defaultFileServer))
|
||||||
|
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList)
|
||||||
|
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList)
|
||||||
putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest))
|
putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest))
|
||||||
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
||||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
||||||
@ -217,6 +221,8 @@ object LocalPreferences {
|
|||||||
|
|
||||||
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
||||||
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
||||||
|
val defaultHomeFollowList = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null)
|
||||||
|
val defaultStoriesFollowList = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null)
|
||||||
|
|
||||||
val zapAmountChoices = gson.fromJson(
|
val zapAmountChoices = gson.fromJson(
|
||||||
getString(PrefKeys.ZAP_AMOUNTS, "[]"),
|
getString(PrefKeys.ZAP_AMOUNTS, "[]"),
|
||||||
@ -278,6 +284,8 @@ object LocalPreferences {
|
|||||||
zapAmountChoices,
|
zapAmountChoices,
|
||||||
defaultZapType,
|
defaultZapType,
|
||||||
defaultFileServer,
|
defaultFileServer,
|
||||||
|
defaultHomeFollowList,
|
||||||
|
defaultStoriesFollowList,
|
||||||
zapPaymentRequestServer,
|
zapPaymentRequestServer,
|
||||||
hideDeleteRequestDialog,
|
hideDeleteRequestDialog,
|
||||||
hideBlockAlertDialog,
|
hideBlockAlertDialog,
|
||||||
|
@ -12,6 +12,7 @@ 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.NostrVideoDataSource
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.Constants
|
import com.vitorpamplona.amethyst.service.relays.Constants
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ object ServiceManager {
|
|||||||
NostrAccountDataSource.account = myAccount
|
NostrAccountDataSource.account = myAccount
|
||||||
NostrHomeDataSource.account = myAccount
|
NostrHomeDataSource.account = myAccount
|
||||||
NostrChatroomListDataSource.account = myAccount
|
NostrChatroomListDataSource.account = myAccount
|
||||||
|
NostrVideoDataSource.account = myAccount
|
||||||
|
|
||||||
// Notification Elements
|
// Notification Elements
|
||||||
NostrHomeDataSource.start()
|
NostrHomeDataSource.start()
|
||||||
@ -61,6 +63,7 @@ object ServiceManager {
|
|||||||
NostrSingleUserDataSource.stop()
|
NostrSingleUserDataSource.stop()
|
||||||
NostrThreadDataSource.stop()
|
NostrThreadDataSource.stop()
|
||||||
NostrUserProfileDataSource.stop()
|
NostrUserProfileDataSource.stop()
|
||||||
|
NostrVideoDataSource.stop()
|
||||||
|
|
||||||
Client.disconnect()
|
Client.disconnect()
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,9 @@ fun getLanguagesSpokenByUser(): Set<String> {
|
|||||||
return codedList
|
return codedList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val GLOBAL_FOLLOWS = " Global "
|
||||||
|
val KIND3_FOLLOWS = " All Follows "
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class Account(
|
class Account(
|
||||||
val loggedIn: Persona,
|
val loggedIn: Persona,
|
||||||
@ -50,6 +53,8 @@ class Account(
|
|||||||
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
||||||
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE,
|
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE,
|
||||||
var defaultFileServer: ServersAvailable = ServersAvailable.IMGUR,
|
var defaultFileServer: ServersAvailable = ServersAvailable.IMGUR,
|
||||||
|
var defaultHomeFollowList: String? = null,
|
||||||
|
var defaultStoriesFollowList: String? = null,
|
||||||
var zapPaymentRequest: Nip47URI? = null,
|
var zapPaymentRequest: Nip47URI? = null,
|
||||||
var hideDeleteRequestDialog: Boolean = false,
|
var hideDeleteRequestDialog: Boolean = false,
|
||||||
var hideBlockAlertDialog: Boolean = false,
|
var hideBlockAlertDialog: Boolean = false,
|
||||||
@ -729,6 +734,18 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeDefaultHomeFollowList(name: String?) {
|
||||||
|
defaultHomeFollowList = name
|
||||||
|
live.invalidateData()
|
||||||
|
saveable.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeDefaultStoriesFollowList(name: String?) {
|
||||||
|
defaultStoriesFollowList = name
|
||||||
|
live.invalidateData()
|
||||||
|
saveable.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
fun changeZapAmounts(newAmounts: List<Long>) {
|
fun changeZapAmounts(newAmounts: List<Long>) {
|
||||||
zapAmountChoices = newAmounts
|
zapAmountChoices = newAmounts
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
@ -741,6 +758,37 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun selectedUsersFollowList(listName: String?): Set<String>? {
|
||||||
|
if (listName == GLOBAL_FOLLOWS) return null
|
||||||
|
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingKeySet()
|
||||||
|
|
||||||
|
val privKey = loggedIn.privKey
|
||||||
|
|
||||||
|
return if (listName != null) {
|
||||||
|
val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag()
|
||||||
|
val list = LocalCache.addressables[aTag]
|
||||||
|
if (list != null) {
|
||||||
|
val publicHexList = (list.event as? PeopleListEvent)?.bookmarkedPeople() ?: emptySet()
|
||||||
|
val privateHexList = privKey?.let {
|
||||||
|
(list.event as? PeopleListEvent)?.privateTaggedUsers(it)
|
||||||
|
} ?: emptySet()
|
||||||
|
|
||||||
|
(publicHexList + privateHexList).toSet()
|
||||||
|
} else {
|
||||||
|
emptySet()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectedTagsFollowList(listName: String?): Set<String>? {
|
||||||
|
if (listName == GLOBAL_FOLLOWS) return null
|
||||||
|
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingTagSet()
|
||||||
|
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) {
|
fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
@ -159,6 +159,20 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(event: PeopleListEvent) {
|
||||||
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event?.id() == event.id()) return
|
||||||
|
|
||||||
|
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun formattedDateTime(timestamp: Long): String {
|
fun formattedDateTime(timestamp: Long): String {
|
||||||
return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
|
return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
|
||||||
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
|
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
|
||||||
|
@ -62,9 +62,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
|||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = COMMON_FEED_TYPES,
|
types = COMMON_FEED_TYPES,
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(BookmarkListEvent.kind),
|
kinds = listOf(BookmarkListEvent.kind, PeopleListEvent.kind),
|
||||||
authors = listOf(account.userProfile().pubkeyHex),
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
limit = 1
|
limit = 100
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,7 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
is LongTextNoteEvent -> LocalCache.consume(event, relay)
|
is LongTextNoteEvent -> LocalCache.consume(event, relay)
|
||||||
is MetadataEvent -> LocalCache.consume(event)
|
is MetadataEvent -> LocalCache.consume(event)
|
||||||
is PrivateDmEvent -> LocalCache.consume(event, relay)
|
is PrivateDmEvent -> LocalCache.consume(event, relay)
|
||||||
|
is PeopleListEvent -> LocalCache.consume(event)
|
||||||
is ReactionEvent -> LocalCache.consume(event)
|
is ReactionEvent -> LocalCache.consume(event)
|
||||||
is RecommendRelayEvent -> LocalCache.consume(event)
|
is RecommendRelayEvent -> LocalCache.consume(event)
|
||||||
is ReportEvent -> LocalCache.consume(event, relay)
|
is ReportEvent -> LocalCache.consume(event, relay)
|
||||||
|
@ -45,7 +45,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createFollowAccountsFilter(): TypedFilter {
|
fun createFollowAccountsFilter(): TypedFilter {
|
||||||
val follows = account.followingKeySet()
|
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
|
|
||||||
val followKeys = follows.map {
|
val followKeys = follows.map {
|
||||||
it.substring(0, 6)
|
it.substring(0, 6)
|
||||||
@ -65,7 +65,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createFollowTagsFilter(): TypedFilter? {
|
fun createFollowTagsFilter(): TypedFilter? {
|
||||||
val hashToLoad = account.followingTagSet()
|
val hashToLoad = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
|
|
||||||
if (hashToLoad.isEmpty()) return null
|
if (hashToLoad.isEmpty()) return null
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
import com.vitorpamplona.amethyst.service.model.FileStorageHeaderEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
@ -7,17 +8,47 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
|||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
|
|
||||||
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||||
fun createGlobalFilter() = TypedFilter(
|
lateinit var account: Account
|
||||||
types = setOf(FeedType.GLOBAL),
|
|
||||||
filter = JsonFilter(
|
fun createContextualFilter(): TypedFilter? {
|
||||||
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
|
val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)
|
||||||
limit = 200
|
|
||||||
|
val followKeys = follows?.map {
|
||||||
|
it.substring(0, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypedFilter(
|
||||||
|
types = setOf(FeedType.GLOBAL),
|
||||||
|
filter = JsonFilter(
|
||||||
|
authors = followKeys,
|
||||||
|
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
|
||||||
|
limit = 200
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
fun createFollowTagsFilter(): TypedFilter? {
|
||||||
|
val hashToLoad = account.selectedTagsFollowList(account.defaultStoriesFollowList)
|
||||||
|
|
||||||
|
if (hashToLoad.isNullOrEmpty()) return null
|
||||||
|
|
||||||
|
return TypedFilter(
|
||||||
|
types = setOf(FeedType.GLOBAL),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
|
||||||
|
tags = mapOf(
|
||||||
|
"t" to hashToLoad.map {
|
||||||
|
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
||||||
|
}.flatten()
|
||||||
|
),
|
||||||
|
limit = 100
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val videoFeedChannel = requestNewChannel()
|
val videoFeedChannel = requestNewChannel()
|
||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
videoFeedChannel.typedFilters = listOf(createGlobalFilter()).ifEmpty { null }
|
videoFeedChannel.typedFilters = listOfNotNull(createContextualFilter(), createFollowTagsFilter()).ifEmpty { null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@ -15,50 +12,7 @@ class BookmarkListEvent(
|
|||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
|
||||||
fun address() = ATag(kind, pubKey, dTag(), null)
|
|
||||||
|
|
||||||
fun category() = dTag()
|
|
||||||
fun bookmarkedPosts() = tags.filter { it[0] == "e" }.mapNotNull { it.getOrNull(1) }
|
|
||||||
|
|
||||||
fun plainContent(privKey: ByteArray): String? {
|
|
||||||
return try {
|
|
||||||
val sharedSecret = Utils.getSharedSecret(privKey, pubKey.toByteArray())
|
|
||||||
|
|
||||||
return Utils.decrypt(content, sharedSecret)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w("BookmarkList", "Error decrypting the message ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transient
|
|
||||||
private var privateTagsCache: List<List<String>>? = null
|
|
||||||
|
|
||||||
fun privateTags(privKey: ByteArray): List<List<String>>? {
|
|
||||||
if (privateTagsCache != null) {
|
|
||||||
return privateTagsCache
|
|
||||||
}
|
|
||||||
|
|
||||||
privateTagsCache = try {
|
|
||||||
gson.fromJson(plainContent(privKey), object : TypeToken<List<List<String>>>() {}.type)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.w("BookmarkList", "Error parsing the JSON ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
return privateTagsCache
|
|
||||||
}
|
|
||||||
|
|
||||||
fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "p" }?.mapNotNull { it.getOrNull(1) }
|
|
||||||
fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "e" }?.mapNotNull { it.getOrNull(1) }
|
|
||||||
fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
|
|
||||||
val aTagValue = it.getOrNull(1)
|
|
||||||
val relay = it.getOrNull(2)
|
|
||||||
|
|
||||||
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 30001
|
const val kind = 30001
|
||||||
|
|
||||||
@ -77,24 +31,7 @@ class BookmarkListEvent(
|
|||||||
createdAt: Long = Date().time / 1000
|
createdAt: Long = Date().time / 1000
|
||||||
): BookmarkListEvent {
|
): BookmarkListEvent {
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||||
|
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
|
||||||
val privTags = mutableListOf<List<String>>()
|
|
||||||
privEvents?.forEach {
|
|
||||||
privTags.add(listOf("e", it))
|
|
||||||
}
|
|
||||||
privUsers?.forEach {
|
|
||||||
privTags.add(listOf("p", it))
|
|
||||||
}
|
|
||||||
privAddresses?.forEach {
|
|
||||||
privTags.add(listOf("a", it.toTag()))
|
|
||||||
}
|
|
||||||
val msg = gson.toJson(privTags)
|
|
||||||
|
|
||||||
val content = Utils.encrypt(
|
|
||||||
msg,
|
|
||||||
privateKey,
|
|
||||||
pubKey
|
|
||||||
)
|
|
||||||
|
|
||||||
val tags = mutableListOf<List<String>>()
|
val tags = mutableListOf<List<String>>()
|
||||||
tags.add(listOf("d", name))
|
tags.add(listOf("d", name))
|
||||||
|
@ -238,6 +238,7 @@ open class Event(
|
|||||||
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
PeopleListEvent.kind -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
PollNoteEvent.kind -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
PollNoteEvent.kind -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
|
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
|
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import nostr.postr.Utils
|
||||||
|
|
||||||
|
abstract class GeneralListEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
kind: Int,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
||||||
|
fun address() = ATag(kind, pubKey, dTag(), null)
|
||||||
|
|
||||||
|
fun category() = dTag()
|
||||||
|
fun bookmarkedPosts() = taggedEvents()
|
||||||
|
fun bookmarkedPeople() = taggedUsers()
|
||||||
|
|
||||||
|
fun plainContent(privKey: ByteArray): String? {
|
||||||
|
return try {
|
||||||
|
val sharedSecret = Utils.getSharedSecret(privKey, pubKey.toByteArray())
|
||||||
|
|
||||||
|
return Utils.decrypt(content, sharedSecret)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("GeneralList", "Error decrypting the message ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private var privateTagsCache: List<List<String>>? = null
|
||||||
|
|
||||||
|
fun privateTags(privKey: ByteArray): List<List<String>>? {
|
||||||
|
if (privateTagsCache != null) {
|
||||||
|
return privateTagsCache
|
||||||
|
}
|
||||||
|
|
||||||
|
privateTagsCache = try {
|
||||||
|
gson.fromJson(plainContent(privKey), object : TypeToken<List<List<String>>>() {}.type)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return privateTagsCache
|
||||||
|
}
|
||||||
|
|
||||||
|
fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] }
|
||||||
|
fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }
|
||||||
|
fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
|
||||||
|
val aTagValue = it.getOrNull(1)
|
||||||
|
val relay = it.getOrNull(2)
|
||||||
|
|
||||||
|
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createPrivateTags(
|
||||||
|
privEvents: List<String>? = null,
|
||||||
|
privUsers: List<String>? = null,
|
||||||
|
privAddresses: List<ATag>? = null,
|
||||||
|
|
||||||
|
privateKey: ByteArray,
|
||||||
|
pubKey: ByteArray
|
||||||
|
): String {
|
||||||
|
val privTags = mutableListOf<List<String>>()
|
||||||
|
privEvents?.forEach {
|
||||||
|
privTags.add(listOf("e", it))
|
||||||
|
}
|
||||||
|
privUsers?.forEach {
|
||||||
|
privTags.add(listOf("p", it))
|
||||||
|
}
|
||||||
|
privAddresses?.forEach {
|
||||||
|
privTags.add(listOf("a", it.toTag()))
|
||||||
|
}
|
||||||
|
val msg = gson.toJson(privTags)
|
||||||
|
|
||||||
|
return Utils.encrypt(
|
||||||
|
msg,
|
||||||
|
privateKey,
|
||||||
|
pubKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import nostr.postr.Utils
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class PeopleListEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
companion object {
|
||||||
|
const val kind = 30000
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
name: String = "",
|
||||||
|
|
||||||
|
events: List<String>? = null,
|
||||||
|
users: List<String>? = null,
|
||||||
|
addresses: List<ATag>? = null,
|
||||||
|
|
||||||
|
privEvents: List<String>? = null,
|
||||||
|
privUsers: List<String>? = null,
|
||||||
|
privAddresses: List<ATag>? = null,
|
||||||
|
|
||||||
|
privateKey: ByteArray,
|
||||||
|
createdAt: Long = Date().time / 1000
|
||||||
|
): PeopleListEvent {
|
||||||
|
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||||
|
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
|
||||||
|
|
||||||
|
val tags = mutableListOf<List<String>>()
|
||||||
|
tags.add(listOf("d", name))
|
||||||
|
|
||||||
|
events?.forEach {
|
||||||
|
tags.add(listOf("e", it))
|
||||||
|
}
|
||||||
|
users?.forEach {
|
||||||
|
tags.add(listOf("p", it))
|
||||||
|
}
|
||||||
|
addresses?.forEach {
|
||||||
|
tags.add(listOf("a", it.toTag()))
|
||||||
|
}
|
||||||
|
|
||||||
|
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
|
||||||
|
val sig = Utils.sign(id, privateKey)
|
||||||
|
return PeopleListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,7 +33,14 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextSpinner(label: String, placeholder: String, options: List<String>, explainers: List<String>? = null, onSelect: (Int) -> Unit, modifier: Modifier = Modifier) {
|
fun TextSpinner(
|
||||||
|
label: String,
|
||||||
|
placeholder: String,
|
||||||
|
options: List<String>,
|
||||||
|
explainers: List<String>? = null,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
var optionsShowing by remember { mutableStateOf(false) }
|
var optionsShowing by remember { mutableStateOf(false) }
|
||||||
|
@ -18,9 +18,8 @@ object HomeConversationsFeedFilter : AdditiveFeedFilter<Note>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
val user = account.userProfile()
|
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
val followingKeySet = user.cachedFollowingKeySet()
|
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
val followingTagSet = user.cachedFollowingTagSet()
|
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
.asSequence()
|
.asSequence()
|
||||||
|
@ -24,9 +24,8 @@ object HomeNewThreadFeedFilter : AdditiveFeedFilter<Note>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
val user = account.userProfile()
|
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
val followingKeySet = user.cachedFollowingKeySet()
|
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
val followingTagSet = user.cachedFollowingTagSet()
|
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
.asSequence()
|
.asSequence()
|
||||||
|
@ -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.PeopleListEvent
|
||||||
|
|
||||||
|
object PeopleListFeedFilter : FeedFilter<Note>() {
|
||||||
|
lateinit var account: Account
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val privKey = account.loggedIn.privKey ?: return emptyList()
|
||||||
|
|
||||||
|
val lists = LocalCache.addressables.values
|
||||||
|
.asSequence()
|
||||||
|
.filter {
|
||||||
|
(it.event is PeopleListEvent)
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
return lists
|
||||||
|
.sortedBy { it.createdAt() }
|
||||||
|
.reversed()
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.dal
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
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.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
@ -20,17 +21,17 @@ object VideoFeedFilter : AdditiveFeedFilter<Note>() {
|
|||||||
|
|
||||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
val now = System.currentTimeMillis() / 1000
|
val now = System.currentTimeMillis() / 1000
|
||||||
|
val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS
|
||||||
|
|
||||||
|
val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet()
|
||||||
|
val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet()
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter {
|
.filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent }
|
||||||
it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent
|
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false) }
|
||||||
}
|
|
||||||
.filter { account.isAcceptable(it) }
|
.filter { account.isAcceptable(it) }
|
||||||
.filter {
|
.filter { it.createdAt()!! <= now }
|
||||||
// Do not show notes with the creation time exceeding the current time, as they will always stay at the top of the global feed, which is cheating.
|
|
||||||
it.createdAt()!! <= now
|
|
||||||
}
|
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.navigation
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -23,6 +24,8 @@ import androidx.compose.material.TopAppBar
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
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
|
||||||
@ -41,6 +44,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
|
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||||
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
|
||||||
@ -55,6 +61,7 @@ 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.model.PeopleListEvent
|
||||||
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.actions.NewRelayListView
|
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
||||||
@ -62,33 +69,58 @@ import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
|||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||||
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 com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
when (currentRoute(navController)) {
|
when (currentRoute(navController)?.substringBefore("?")) {
|
||||||
// Route.Profile.route -> TopBarWithBackButton(navController)
|
// Route.Profile.route -> TopBarWithBackButton(navController)
|
||||||
|
Route.Home.base -> HomeTopBar(scaffoldState, accountViewModel)
|
||||||
|
Route.Video.base -> StoriesTopBar(scaffoldState, accountViewModel)
|
||||||
else -> MainTopBar(scaffoldState, accountViewModel)
|
else -> MainTopBar(scaffoldState, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StoriesTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
|
FollowList(account.defaultStoriesFollowList, true) { listName ->
|
||||||
|
account.changeDefaultStoriesFollowList(listName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
|
FollowList(account.defaultHomeFollowList, false) { listName ->
|
||||||
|
account.changeDefaultHomeFollowList(listName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
|
GenericTopBar(scaffoldState, accountViewModel) {
|
||||||
|
AmethystIcon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(coil.annotation.ExperimentalCoilApi::class)
|
@OptIn(coil.annotation.ExperimentalCoilApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, content: @Composable (Account) -> Unit) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
val accountUserState by account.userProfile().live().metadata.observeAsState()
|
|
||||||
val accountUser = accountUserState?.user ?: return
|
|
||||||
|
|
||||||
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
|
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
|
||||||
val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState()
|
val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState()
|
||||||
val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState()
|
val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState()
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var wantsToEditRelays by remember {
|
var wantsToEditRelays by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@ -115,48 +147,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
IconButton(
|
content(account)
|
||||||
onClick = {
|
|
||||||
Client.allSubscriptions().map {
|
|
||||||
"$it ${
|
|
||||||
Client.getSubscriptionFilters(it)
|
|
||||||
.joinToString { it.filter.toJson() }
|
|
||||||
}"
|
|
||||||
}.forEach {
|
|
||||||
Log.d("STATE DUMP", it)
|
|
||||||
}
|
|
||||||
|
|
||||||
NostrAccountDataSource.printCounter()
|
|
||||||
NostrChannelDataSource.printCounter()
|
|
||||||
NostrChatroomDataSource.printCounter()
|
|
||||||
NostrChatroomListDataSource.printCounter()
|
|
||||||
NostrGlobalDataSource.printCounter()
|
|
||||||
NostrHashtagDataSource.printCounter()
|
|
||||||
NostrHomeDataSource.printCounter()
|
|
||||||
NostrSearchEventOrUserDataSource.printCounter()
|
|
||||||
NostrSingleChannelDataSource.printCounter()
|
|
||||||
NostrSingleEventDataSource.printCounter()
|
|
||||||
NostrSingleUserDataSource.printCounter()
|
|
||||||
NostrThreadDataSource.printCounter()
|
|
||||||
NostrUserProfileDataSource.printCounter()
|
|
||||||
|
|
||||||
Log.d("STATE DUMP", "Connected Relays: " + RelayPool.connectedRelays())
|
|
||||||
|
|
||||||
val imageLoader = Coil.imageLoader(context)
|
|
||||||
Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
|
||||||
Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
|
||||||
|
|
||||||
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size)
|
|
||||||
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.amethyst),
|
|
||||||
null,
|
|
||||||
modifier = Modifier.size(40.dp),
|
|
||||||
tint = Color.Unspecified
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@ -186,23 +177,10 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(
|
LoggedInUserPictureDrawer(account) {
|
||||||
onClick = {
|
coroutineScope.launch {
|
||||||
coroutineScope.launch {
|
scaffoldState.drawerState.open()
|
||||||
scaffoldState.drawerState.open()
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
) {
|
|
||||||
RobohashAsyncImageProxy(
|
|
||||||
robot = accountUser.pubkeyHex,
|
|
||||||
model = ResizeImage(accountUser.profilePicture(), 34.dp),
|
|
||||||
contentDescription = stringResource(id = R.string.profile_image),
|
|
||||||
modifier = Modifier
|
|
||||||
.width(34.dp)
|
|
||||||
.height(34.dp)
|
|
||||||
.clip(shape = CircleShape)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
@ -223,6 +201,107 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LoggedInUserPictureDrawer(
|
||||||
|
account: Account,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val accountUserState by account.userProfile().live().metadata.observeAsState()
|
||||||
|
val accountUser = accountUserState?.user ?: return
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier
|
||||||
|
) {
|
||||||
|
RobohashAsyncImageProxy(
|
||||||
|
robot = accountUser.pubkeyHex,
|
||||||
|
model = ResizeImage(accountUser.profilePicture(), 34.dp),
|
||||||
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
|
modifier = Modifier
|
||||||
|
.width(34.dp)
|
||||||
|
.height(34.dp)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowList(listName: String?, withGlobal: Boolean, onChange: (String?) -> Unit) {
|
||||||
|
// Notification
|
||||||
|
val dbState = LocalCache.live.observeAsState()
|
||||||
|
val db = dbState.value ?: return
|
||||||
|
|
||||||
|
val kind3Follow = Pair(KIND3_FOLLOWS, stringResource(id = R.string.follow_list_kind3follows))
|
||||||
|
val globalFollow = Pair(GLOBAL_FOLLOWS, stringResource(id = R.string.follow_list_global))
|
||||||
|
|
||||||
|
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
|
||||||
|
|
||||||
|
var followLists by remember { mutableStateOf(defaultOptions) }
|
||||||
|
val followNames = remember { derivedStateOf { followLists.map { it.second } } }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = db) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
followLists = defaultOptions + LocalCache.addressables.mapNotNull {
|
||||||
|
val event = (it.value.event as? PeopleListEvent)
|
||||||
|
// Has to have an list
|
||||||
|
if (event != null && (event.tags.size > 1 || event.content.length > 50)) {
|
||||||
|
Pair(event.dTag(), event.dTag())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.sortedBy { it.second }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleTextSpinner(
|
||||||
|
placeholder = followLists.firstOrNull { it.first == listName }?.first ?: KIND3_FOLLOWS,
|
||||||
|
options = followNames.value,
|
||||||
|
onSelect = {
|
||||||
|
onChange(followLists.getOrNull(it)?.first)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SimpleTextSpinner(
|
||||||
|
placeholder: String,
|
||||||
|
options: List<String>,
|
||||||
|
explainers: List<String>? = null,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
var optionsShowing by remember { mutableStateOf(false) }
|
||||||
|
var currentText by remember { mutableStateOf(placeholder) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier,
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(currentText)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null
|
||||||
|
) {
|
||||||
|
optionsShowing = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionsShowing) {
|
||||||
|
options.isNotEmpty().also {
|
||||||
|
SpinnerSelectionDialog(options = options, explainers = explainers, onDismiss = { optionsShowing = false }) {
|
||||||
|
currentText = options[it]
|
||||||
|
optionsShowing = false
|
||||||
|
onSelect(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopBarWithBackButton(navController: NavHostController) {
|
fun TopBarWithBackButton(navController: NavHostController) {
|
||||||
Column() {
|
Column() {
|
||||||
@ -250,3 +329,51 @@ fun TopBarWithBackButton(navController: NavHostController) {
|
|||||||
Divider(thickness = 0.25.dp)
|
Divider(thickness = 0.25.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AmethystIcon() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
Client.allSubscriptions().map {
|
||||||
|
"$it ${
|
||||||
|
Client.getSubscriptionFilters(it)
|
||||||
|
.joinToString { it.filter.toJson() }
|
||||||
|
}"
|
||||||
|
}.forEach {
|
||||||
|
Log.d("STATE DUMP", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
NostrAccountDataSource.printCounter()
|
||||||
|
NostrChannelDataSource.printCounter()
|
||||||
|
NostrChatroomDataSource.printCounter()
|
||||||
|
NostrChatroomListDataSource.printCounter()
|
||||||
|
NostrGlobalDataSource.printCounter()
|
||||||
|
NostrHashtagDataSource.printCounter()
|
||||||
|
NostrHomeDataSource.printCounter()
|
||||||
|
NostrSearchEventOrUserDataSource.printCounter()
|
||||||
|
NostrSingleChannelDataSource.printCounter()
|
||||||
|
NostrSingleEventDataSource.printCounter()
|
||||||
|
NostrSingleUserDataSource.printCounter()
|
||||||
|
NostrThreadDataSource.printCounter()
|
||||||
|
NostrUserProfileDataSource.printCounter()
|
||||||
|
|
||||||
|
Log.d("STATE DUMP", "Connected Relays: " + RelayPool.connectedRelays())
|
||||||
|
|
||||||
|
val imageLoader = Coil.imageLoader(context)
|
||||||
|
Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
||||||
|
Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
||||||
|
|
||||||
|
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size)
|
||||||
|
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.amethyst),
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
tint = Color.Unspecified
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.DisposableEffect
|
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.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@ -50,9 +51,11 @@ fun HomeScreen(
|
|||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val account = accountViewModel.accountLiveData.value?.account ?: return
|
val account = accountViewModel.accountLiveData.value?.account ?: return
|
||||||
var wantsToAddNip47 by remember { mutableStateOf<String?>(nip47) }
|
var wantsToAddNip47 by remember { mutableStateOf(nip47) }
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
val accountState = account.live.observeAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(accountViewModel, accountState.value?.account?.defaultHomeFollowList) {
|
||||||
HomeNewThreadFeedFilter.account = account
|
HomeNewThreadFeedFilter.account = account
|
||||||
HomeConversationsFeedFilter.account = account
|
HomeConversationsFeedFilter.account = account
|
||||||
NostrHomeDataSource.resetFilters()
|
NostrHomeDataSource.resetFilters()
|
||||||
|
@ -105,10 +105,14 @@ fun VideoScreen(
|
|||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
val account = accountViewModel.accountLiveData.value?.account ?: return
|
val account = accountViewModel.accountLiveData.value?.account ?: return
|
||||||
|
|
||||||
|
val accountState = account.live.observeAsState()
|
||||||
|
|
||||||
|
NostrVideoDataSource.account = account
|
||||||
VideoFeedFilter.account = account
|
VideoFeedFilter.account = account
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
LaunchedEffect(accountViewModel, accountState.value?.account?.defaultStoriesFollowList) {
|
||||||
VideoFeedFilter.account = account
|
VideoFeedFilter.account = account
|
||||||
|
NostrVideoDataSource.account = account
|
||||||
NostrVideoDataSource.resetFilters()
|
NostrVideoDataSource.resetFilters()
|
||||||
videoFeedView.invalidateData()
|
videoFeedView.invalidateData()
|
||||||
}
|
}
|
||||||
@ -118,6 +122,7 @@ fun VideoScreen(
|
|||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
println("Video Start")
|
println("Video Start")
|
||||||
VideoFeedFilter.account = account
|
VideoFeedFilter.account = account
|
||||||
|
NostrVideoDataSource.account = account
|
||||||
NostrVideoDataSource.start()
|
NostrVideoDataSource.start()
|
||||||
videoFeedView.invalidateData()
|
videoFeedView.invalidateData()
|
||||||
}
|
}
|
||||||
|
@ -348,4 +348,10 @@
|
|||||||
<string name="upload_server_relays_nip95">Your relays (NIP-95)</string>
|
<string name="upload_server_relays_nip95">Your relays (NIP-95)</string>
|
||||||
<string name="upload_server_relays_nip95_explainer">Files are hosted by your relays. New NIP: check if they support</string>
|
<string name="upload_server_relays_nip95_explainer">Files are hosted by your relays. New NIP: check if they support</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<string name="follow_list_selection">Follow List</string>
|
||||||
|
<string name="follow_list_kind3follows">All Follows</string>
|
||||||
|
<string name="follow_list_global">Global</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user