mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-11 02:33:49 +02:00
Read only support for Badges.
This commit is contained in:
@@ -6,6 +6,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature
|
|||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.vitorpamplona.amethyst.service.model.ATag
|
import com.vitorpamplona.amethyst.service.model.ATag
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
@@ -138,7 +141,6 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun consume(event: MetadataEvent) {
|
fun consume(event: MetadataEvent) {
|
||||||
// new event
|
// new event
|
||||||
val oldUser = getOrCreateUser(event.pubKey)
|
val oldUser = getOrCreateUser(event.pubKey)
|
||||||
@@ -252,6 +254,71 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(event: BadgeDefinitionEvent) {
|
||||||
|
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<User>(), emptyList<Note>())
|
||||||
|
|
||||||
|
refreshObservers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(event: BadgeProfilesEvent) {
|
||||||
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event?.id == event.id) return
|
||||||
|
|
||||||
|
val replyTo = event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
|
event.badgeAwardDefinitions().mapNotNull { getOrCreateAddressableNote(it) }
|
||||||
|
|
||||||
|
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||||
|
note.loadEvent(event, author, emptyList(), replyTo)
|
||||||
|
|
||||||
|
author.updateAcceptedBadges(note)
|
||||||
|
|
||||||
|
refreshObservers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(event: BadgeAwardEvent) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
//Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
|
||||||
|
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
val awardees = event.awardees().mapNotNull { checkGetOrCreateUser(it) }
|
||||||
|
val awardDefinition = event.awardDefinition().map { getOrCreateAddressableNote(it) }
|
||||||
|
|
||||||
|
note.loadEvent(event, author, awardees, awardDefinition)
|
||||||
|
|
||||||
|
// Adds notifications to users.
|
||||||
|
awardees.forEach {
|
||||||
|
it.addTaggedPost(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Counts the replies
|
||||||
|
awardees.forEach {
|
||||||
|
it.addBadgeAward(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replies of an Badge Definition are Award Events
|
||||||
|
awardDefinition.forEach {
|
||||||
|
it.addReply(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshObservers()
|
||||||
|
}
|
||||||
|
|
||||||
private fun findCitations(event: Event): Set<String> {
|
private fun findCitations(event: Event): Set<String> {
|
||||||
var citations = mutableSetOf<String>()
|
var citations = mutableSetOf<String>()
|
||||||
// Removes citations from replies:
|
// Removes citations from replies:
|
||||||
@@ -537,6 +604,7 @@ object LocalCache {
|
|||||||
// older data, does nothing
|
// older data, does nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: ChannelMetadataEvent) {
|
fun consume(event: ChannelMetadataEvent) {
|
||||||
val channelId = event.channel()
|
val channelId = event.channel()
|
||||||
//Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}")
|
//Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}")
|
||||||
|
@@ -24,6 +24,8 @@ import nostr.postr.toNpub
|
|||||||
|
|
||||||
val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)")
|
val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)")
|
||||||
|
|
||||||
|
class Badges(val definition: Note, val awardees: Set<Note>)
|
||||||
|
|
||||||
class User(val pubkeyHex: String) {
|
class User(val pubkeyHex: String) {
|
||||||
var info: UserMetadata? = null
|
var info: UserMetadata? = null
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ class User(val pubkeyHex: String) {
|
|||||||
|
|
||||||
var reports = mapOf<User, Set<Note>>()
|
var reports = mapOf<User, Set<Note>>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var latestReportTime: Long = 0
|
var latestReportTime: Long = 0
|
||||||
|
|
||||||
var zaps = mapOf<Note, Note?>()
|
var zaps = mapOf<Note, Note?>()
|
||||||
@@ -57,6 +60,11 @@ class User(val pubkeyHex: String) {
|
|||||||
var privateChatrooms = mapOf<User, Chatroom>()
|
var privateChatrooms = mapOf<User, Chatroom>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var badgeAwards = setOf<Note>()
|
||||||
|
private set
|
||||||
|
|
||||||
|
var acceptedBadges: AddressableNote? = null
|
||||||
|
|
||||||
fun pubkey() = Hex.decode(pubkeyHex)
|
fun pubkey() = Hex.decode(pubkeyHex)
|
||||||
fun pubkeyNpub() = pubkey().toNpub()
|
fun pubkeyNpub() = pubkey().toNpub()
|
||||||
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
|
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
|
||||||
@@ -175,6 +183,23 @@ class User(val pubkeyHex: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addBadgeAward(note: Note) {
|
||||||
|
if (note !in badgeAwards) {
|
||||||
|
badgeAwards = badgeAwards + note
|
||||||
|
liveSet?.badges?.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeBadgeAward(deleteNote: Note) {
|
||||||
|
badgeAwards = badgeAwards - deleteNote
|
||||||
|
liveSet?.badges?.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAcceptedBadges(note: AddressableNote) {
|
||||||
|
acceptedBadges = note
|
||||||
|
liveSet?.badges?.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
fun addZap(zapRequest: Note, zap: Note?) {
|
fun addZap(zapRequest: Note, zap: Note?) {
|
||||||
if (zapRequest !in zaps.keys) {
|
if (zapRequest !in zaps.keys) {
|
||||||
zaps = zaps + Pair(zapRequest, zap)
|
zaps = zaps + Pair(zapRequest, zap)
|
||||||
@@ -316,6 +341,8 @@ class User(val pubkeyHex: String) {
|
|||||||
liveSet = null
|
liveSet = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserLiveSet(u: User) {
|
class UserLiveSet(u: User) {
|
||||||
@@ -327,6 +354,7 @@ class UserLiveSet(u: User) {
|
|||||||
val relayInfo: UserLiveData = UserLiveData(u)
|
val relayInfo: UserLiveData = UserLiveData(u)
|
||||||
val metadata: UserLiveData = UserLiveData(u)
|
val metadata: UserLiveData = UserLiveData(u)
|
||||||
val zaps: UserLiveData = UserLiveData(u)
|
val zaps: UserLiveData = UserLiveData(u)
|
||||||
|
val badges: UserLiveData = UserLiveData(u)
|
||||||
|
|
||||||
fun isInUse(): Boolean {
|
fun isInUse(): Boolean {
|
||||||
return follows.hasObservers()
|
return follows.hasObservers()
|
||||||
@@ -336,6 +364,7 @@ class UserLiveSet(u: User) {
|
|||||||
|| relayInfo.hasObservers()
|
|| relayInfo.hasObservers()
|
||||||
|| metadata.hasObservers()
|
|| metadata.hasObservers()
|
||||||
|| zaps.hasObservers()
|
|| zaps.hasObservers()
|
||||||
|
|| badges.hasObservers()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
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
|
||||||
@@ -38,6 +40,17 @@ object NostrAccountDataSource: NostrDataSource("AccountData") {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createAccountAcceptedAwardsFilter(): TypedFilter {
|
||||||
|
return TypedFilter(
|
||||||
|
types = FeedType.values().toSet(),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(BadgeProfilesEvent.kind),
|
||||||
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
|
limit = 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun createAccountReportsFilter(): TypedFilter {
|
fun createAccountReportsFilter(): TypedFilter {
|
||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = FeedType.values().toSet(),
|
types = FeedType.values().toSet(),
|
||||||
@@ -52,7 +65,7 @@ object NostrAccountDataSource: NostrDataSource("AccountData") {
|
|||||||
types = FeedType.values().toSet(),
|
types = FeedType.values().toSet(),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(
|
kinds = listOf(
|
||||||
TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, ChannelMessageEvent.kind
|
TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, ChannelMessageEvent.kind, BadgeAwardEvent.kind
|
||||||
),
|
),
|
||||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||||
limit = 200
|
limit = 200
|
||||||
@@ -67,7 +80,8 @@ object NostrAccountDataSource: NostrDataSource("AccountData") {
|
|||||||
createAccountMetadataFilter(),
|
createAccountMetadataFilter(),
|
||||||
createAccountContactListFilter(),
|
createAccountContactListFilter(),
|
||||||
createNotificationFilter(),
|
createNotificationFilter(),
|
||||||
createAccountReportsFilter()
|
createAccountReportsFilter(),
|
||||||
|
createAccountAcceptedAwardsFilter()
|
||||||
).ifEmpty { null }
|
).ifEmpty { null }
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,6 +2,9 @@ package com.vitorpamplona.amethyst.service
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
@@ -62,6 +65,9 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
is BadgeAwardEvent -> LocalCache.consume(event)
|
||||||
|
is BadgeDefinitionEvent -> LocalCache.consume(event)
|
||||||
|
is BadgeProfilesEvent -> LocalCache.consume(event)
|
||||||
is ChannelCreateEvent -> LocalCache.consume(event)
|
is ChannelCreateEvent -> LocalCache.consume(event)
|
||||||
is ChannelHideMessageEvent -> LocalCache.consume(event)
|
is ChannelHideMessageEvent -> LocalCache.consume(event)
|
||||||
is ChannelMessageEvent -> LocalCache.consume(event, relay)
|
is ChannelMessageEvent -> LocalCache.consume(event, relay)
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||||
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
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
@@ -20,7 +24,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
private var eventsToWatch = setOf<Note>()
|
private var eventsToWatch = setOf<Note>()
|
||||||
private var addressesToWatch = setOf<Note>()
|
private var addressesToWatch = setOf<Note>()
|
||||||
|
|
||||||
private fun createAddressFilter(): List<TypedFilter>? {
|
private fun createTagToAddressFilter(): List<TypedFilter>? {
|
||||||
val addressesToWatch = eventsToWatch.filter { it.address() != null } + addressesToWatch
|
val addressesToWatch = eventsToWatch.filter { it.address() != null } + addressesToWatch
|
||||||
|
|
||||||
if (addressesToWatch.isEmpty()) {
|
if (addressesToWatch.isEmpty()) {
|
||||||
@@ -38,7 +42,10 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
types = FeedType.values().toSet(),
|
types = FeedType.values().toSet(),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(
|
kinds = listOf(
|
||||||
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind
|
TextNoteEvent.kind, LongTextNoteEvent.kind,
|
||||||
|
ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind,
|
||||||
|
LnZapEvent.kind, LnZapRequestEvent.kind,
|
||||||
|
BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind
|
||||||
),
|
),
|
||||||
tags = mapOf("a" to listOf(aTag.toTag())),
|
tags = mapOf("a" to listOf(aTag.toTag())),
|
||||||
since = it.lastReactionsDownloadTime
|
since = it.lastReactionsDownloadTime
|
||||||
@@ -48,6 +55,32 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createAddressFilter(): List<TypedFilter>? {
|
||||||
|
val addressesToWatch = addressesToWatch.filter { it.event == null }
|
||||||
|
|
||||||
|
if (addressesToWatch.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = Date().time / 1000
|
||||||
|
|
||||||
|
return addressesToWatch.filter {
|
||||||
|
val lastTime = it.lastReactionsDownloadTime
|
||||||
|
lastTime == null || lastTime < (now - 10)
|
||||||
|
}.mapNotNull {
|
||||||
|
it.address()?.let { aTag ->
|
||||||
|
TypedFilter(
|
||||||
|
types = FeedType.values().toSet(),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(aTag.kind),
|
||||||
|
tags = mapOf("d" to listOf(aTag.dTag)),
|
||||||
|
authors = listOf(aTag.pubKeyHex.substring(0,8))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
|
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
|
||||||
val reactionsToWatch = eventsToWatch
|
val reactionsToWatch = eventsToWatch
|
||||||
|
|
||||||
@@ -81,7 +114,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
val threadingEventsToLoad = eventsToWatch
|
val threadingEventsToLoad = eventsToWatch
|
||||||
.mapNotNull { it.replyTo }
|
.mapNotNull { it.replyTo }
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter { it.event == null }
|
.filter { it !is AddressableNote && it.event == null }
|
||||||
|
|
||||||
val interestedEvents =
|
val interestedEvents =
|
||||||
(directEventsToLoad + threadingEventsToLoad)
|
(directEventsToLoad + threadingEventsToLoad)
|
||||||
@@ -98,7 +131,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(
|
kinds = listOf(
|
||||||
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind,
|
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind,
|
||||||
ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind
|
ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind, BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind
|
||||||
),
|
),
|
||||||
ids = interestedEvents.toList()
|
ids = interestedEvents.toList()
|
||||||
)
|
)
|
||||||
@@ -119,8 +152,9 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
|
|||||||
val reactions = createRepliesAndReactionsFilter()
|
val reactions = createRepliesAndReactionsFilter()
|
||||||
val missing = createLoadEventsIfNotLoadedFilter()
|
val missing = createLoadEventsIfNotLoadedFilter()
|
||||||
val addresses = createAddressFilter()
|
val addresses = createAddressFilter()
|
||||||
|
val addressReactions = createTagToAddressFilter()
|
||||||
|
|
||||||
singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses).flatten().ifEmpty { null }
|
singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses, addressReactions).flatten().ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(eventId: Note) {
|
fun add(eventId: Note) {
|
||||||
|
@@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.service
|
|||||||
|
|
||||||
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 com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
@@ -77,6 +79,28 @@ object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createAcceptedAwardsFilter() = user?.let {
|
||||||
|
TypedFilter(
|
||||||
|
types = FeedType.values().toSet(),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(BadgeProfilesEvent.kind),
|
||||||
|
tags = mapOf("p" to listOf(it.pubkeyHex)),
|
||||||
|
limit = 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createReceivedAwardsFilter() = user?.let {
|
||||||
|
TypedFilter(
|
||||||
|
types = FeedType.values().toSet(),
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(BadgeAwardEvent.kind),
|
||||||
|
tags = mapOf("p" to listOf(it.pubkeyHex)),
|
||||||
|
limit = 20
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val userInfoChannel = requestNewChannel()
|
val userInfoChannel = requestNewChannel()
|
||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
@@ -85,7 +109,9 @@ object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
|
|||||||
createUserPostsFilter(),
|
createUserPostsFilter(),
|
||||||
createFollowFilter(),
|
createFollowFilter(),
|
||||||
createFollowersFilter(),
|
createFollowersFilter(),
|
||||||
createUserReceivedZapsFilter()
|
createUserReceivedZapsFilter(),
|
||||||
|
createAcceptedAwardsFilter(),
|
||||||
|
createReceivedAwardsFilter()
|
||||||
).ifEmpty { null }
|
).ifEmpty { null }
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import java.util.Date
|
||||||
|
import nostr.postr.Utils
|
||||||
|
|
||||||
|
class BadgeAwardEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
fun awardees() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
fun awardDefinition() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 8
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import java.util.Date
|
||||||
|
import nostr.postr.Utils
|
||||||
|
|
||||||
|
class BadgeDefinitionEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
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())
|
||||||
|
|
||||||
|
fun name() = tags.filter { it.firstOrNull() == "name" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
fun thumb() = tags.filter { it.firstOrNull() == "thumb" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
fun image() = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
fun description() = tags.filter { it.firstOrNull() == "description" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 30009
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
|
||||||
|
class BadgeProfilesEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
|
||||||
|
|
||||||
|
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
||||||
|
fun address() = ATag(kind, pubKey, dTag())
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 30008
|
||||||
|
}
|
||||||
|
}
|
@@ -161,6 +161,10 @@ open class Event(
|
|||||||
fun fromJson(json: JsonElement, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient)
|
fun fromJson(json: JsonElement, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient)
|
||||||
|
|
||||||
fun Event.getRefinedEvent(lenient: Boolean = false): Event = when (kind) {
|
fun Event.getRefinedEvent(lenient: Boolean = false): Event = when (kind) {
|
||||||
|
BadgeAwardEvent.kind -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
BadgeProfilesEvent.kind -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|
||||||
ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
|
ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
@@ -19,6 +19,8 @@ object NotificationFeedFilter: FeedFilter<Note>() {
|
|||||||
it.event !is ChannelCreateEvent
|
it.event !is ChannelCreateEvent
|
||||||
&& it.event !is ChannelMetadataEvent
|
&& it.event !is ChannelMetadataEvent
|
||||||
&& it.event !is LnZapRequestEvent
|
&& it.event !is LnZapRequestEvent
|
||||||
|
&& it.event !is BadgeDefinitionEvent
|
||||||
|
&& it.event !is BadgeProfilesEvent
|
||||||
}
|
}
|
||||||
.filter { it ->
|
.filter { it ->
|
||||||
it.event !is TextNoteEvent
|
it.event !is TextNoteEvent
|
||||||
|
@@ -0,0 +1,141 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Badge
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.MilitaryTech
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.compositeOver
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
|
import com.vitorpamplona.amethyst.NotificationCache
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun BadgeCompose(likeSetCard: BadgeCard, modifier: Modifier = Modifier, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
|
val noteState by likeSetCard.note.live().metadata.observeAsState()
|
||||||
|
val note = noteState?.note
|
||||||
|
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
|
val context = LocalContext.current.applicationContext
|
||||||
|
|
||||||
|
val noteEvent = note?.event
|
||||||
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (note == null) {
|
||||||
|
BlankNote(Modifier, isInnerNote)
|
||||||
|
} else {
|
||||||
|
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = likeSetCard) {
|
||||||
|
isNew = likeSetCard.createdAt() > NotificationCache.load(routeForLastRead, context)
|
||||||
|
|
||||||
|
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt(), context)
|
||||||
|
}
|
||||||
|
|
||||||
|
var backgroundColor = if (isNew) {
|
||||||
|
MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colors.background
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.background(backgroundColor).combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
if (noteEvent !is ChannelMessageEvent) {
|
||||||
|
navController.navigate("Note/${note.idHex}"){
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
note.channel()?.let {
|
||||||
|
navController.navigate("Channel/${it.idHex}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { popupExpanded = true }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
start = if (!isInnerNote) 12.dp else 0.dp,
|
||||||
|
end = if (!isInnerNote) 12.dp else 0.dp,
|
||||||
|
top = 10.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Draws the like picture outside the boosted card.
|
||||||
|
if (!isInnerNote) {
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.width(55.dp)
|
||||||
|
.padding(0.dp)) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MilitaryTech,
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(25.dp).align(Alignment.TopEnd),
|
||||||
|
tint = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.new_badge_award_notif),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(bottom = 5.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
note.replyTo?.firstOrNull()?.let {
|
||||||
|
NoteCompose(
|
||||||
|
baseNote = it,
|
||||||
|
routeForLastRead = null,
|
||||||
|
isBoostedNote = true,
|
||||||
|
parentBackgroundColor = backgroundColor,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(top = 10.dp),
|
||||||
|
thickness = 0.25.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.note
|
|||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.CutCornerShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -26,6 +27,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
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.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -38,6 +40,8 @@ import com.vitorpamplona.amethyst.RoboHashCache
|
|||||||
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.BadgeAwardEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
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
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
@@ -106,6 +110,8 @@ fun NoteCompose(
|
|||||||
)
|
)
|
||||||
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
|
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
|
||||||
ChannelHeader(baseChannel = baseChannel, account = account, navController = navController)
|
ChannelHeader(baseChannel = baseChannel, account = account, navController = navController)
|
||||||
|
} else if (noteEvent is BadgeDefinitionEvent) {
|
||||||
|
BadgeDisplay(baseNote = note)
|
||||||
} else {
|
} else {
|
||||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||||
|
|
||||||
@@ -354,6 +360,39 @@ fun NoteCompose(
|
|||||||
|
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel)
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(top = 10.dp),
|
||||||
|
thickness = 0.25.dp
|
||||||
|
)
|
||||||
|
} else if (noteEvent is BadgeAwardEvent && !note.replyTo.isNullOrEmpty()) {
|
||||||
|
Text(text = stringResource(R.string.award_granted_to))
|
||||||
|
|
||||||
|
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
||||||
|
note.mentions?.forEach {
|
||||||
|
UserPicture(
|
||||||
|
user = it,
|
||||||
|
navController = navController,
|
||||||
|
userAccount = account.userProfile(),
|
||||||
|
size = 35.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
note.replyTo?.firstOrNull()?.let {
|
||||||
|
NoteCompose(
|
||||||
|
it,
|
||||||
|
modifier = Modifier,
|
||||||
|
isBoostedNote = false,
|
||||||
|
isQuotedNote = true,
|
||||||
|
unPackReply = false,
|
||||||
|
parentBackgroundColor = backgroundColor,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactionsRow(note, accountViewModel)
|
||||||
|
|
||||||
Divider(
|
Divider(
|
||||||
modifier = Modifier.padding(top = 10.dp),
|
modifier = Modifier.padding(top = 10.dp),
|
||||||
thickness = 0.25.dp
|
thickness = 0.25.dp
|
||||||
@@ -393,6 +432,61 @@ fun NoteCompose(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BadgeDisplay(baseNote: Note) {
|
||||||
|
val badgeData = baseNote.event as? BadgeDefinitionEvent ?: return
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(10.dp)
|
||||||
|
.clip(shape = CutCornerShape(20, 20, 0, 0))
|
||||||
|
.border(
|
||||||
|
5.dp,
|
||||||
|
MaterialTheme.colors.primary.copy(alpha = 0.32f),
|
||||||
|
CutCornerShape(20)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
badgeData.image()?.let {
|
||||||
|
AsyncImage(
|
||||||
|
model = it,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
R.string.badge_award_image_for,
|
||||||
|
it
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
badgeData.name()?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
badgeData.description()?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LongFormHeader(noteEvent: LongTextNoteEvent) {
|
private fun LongFormHeader(noteEvent: LongTextNoteEvent) {
|
||||||
Row(
|
Row(
|
||||||
|
@@ -8,6 +8,14 @@ abstract class Card() {
|
|||||||
abstract fun id(): String
|
abstract fun id(): String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BadgeCard(val note: Note): Card() {
|
||||||
|
override fun createdAt(): Long {
|
||||||
|
return note.createdAt() ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun id() = note.idHex
|
||||||
|
}
|
||||||
|
|
||||||
class NoteCard(val note: Note): Card() {
|
class NoteCard(val note: Note): Card() {
|
||||||
override fun createdAt(): Long {
|
override fun createdAt(): Long {
|
||||||
return note.createdAt() ?: 0
|
return note.createdAt() ?: 0
|
||||||
|
@@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.BoostSetCompose
|
import com.vitorpamplona.amethyst.ui.note.BoostSetCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.LikeSetCompose
|
import com.vitorpamplona.amethyst.ui.note.LikeSetCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.MultiSetCompose
|
import com.vitorpamplona.amethyst.ui.note.MultiSetCompose
|
||||||
@@ -127,6 +128,12 @@ private fun FeedLoaded(
|
|||||||
navController = navController,
|
navController = navController,
|
||||||
routeForLastRead = routeForLastRead
|
routeForLastRead = routeForLastRead
|
||||||
)
|
)
|
||||||
|
is BadgeCard -> BadgeCompose(
|
||||||
|
item,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
navController = navController,
|
||||||
|
routeForLastRead = routeForLastRead
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
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.model.BadgeAwardEvent
|
||||||
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
|
||||||
@@ -106,7 +107,12 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>): ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map { NoteCard(it) }
|
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map {
|
||||||
|
if (it.event is BadgeAwardEvent)
|
||||||
|
BadgeCard(it)
|
||||||
|
else
|
||||||
|
NoteCard(it)
|
||||||
|
}
|
||||||
|
|
||||||
return (multiCards + textNoteCards).sortedBy { it.createdAt() }.reversed()
|
return (multiCards + textNoteCards).sortedBy { it.createdAt() }.reversed()
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -58,7 +59,9 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) {
|
fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
@@ -267,7 +270,10 @@ fun NoteMaster(baseNote: Note,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noteEvent is LongTextNoteEvent) {
|
if (noteEvent is BadgeDefinitionEvent) {
|
||||||
|
Spacer(modifier = Modifier.padding(top=10.dp))
|
||||||
|
BadgeDisplay(baseNote = note)
|
||||||
|
} else if (noteEvent is LongTextNoteEvent) {
|
||||||
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)) {
|
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)) {
|
||||||
Column {
|
Column {
|
||||||
noteEvent.image()?.let {
|
noteEvent.image()?.let {
|
||||||
|
@@ -17,8 +17,10 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
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.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
@@ -34,6 +36,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
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.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
|
||||||
@@ -41,14 +44,20 @@ 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.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.RoboHashCache
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
||||||
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
|
||||||
@@ -62,6 +71,7 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||||
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.FeedView
|
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
||||||
@@ -356,7 +366,7 @@ private fun ProfileHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawAdditionalInfo(baseUser, account)
|
DrawAdditionalInfo(baseUser, account, navController)
|
||||||
|
|
||||||
Divider(modifier = Modifier.padding(top = 6.dp))
|
Divider(modifier = Modifier.padding(top = 6.dp))
|
||||||
}
|
}
|
||||||
@@ -367,11 +377,15 @@ private fun ProfileHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun DrawAdditionalInfo(baseUser: User, account: Account) {
|
private fun DrawAdditionalInfo(baseUser: User, account: Account, navController: NavController) {
|
||||||
val userState by baseUser.live().metadata.observeAsState()
|
val userState by baseUser.live().metadata.observeAsState()
|
||||||
val user = userState?.user ?: return
|
val user = userState?.user ?: return
|
||||||
|
|
||||||
|
val userBadgeState by baseUser.live().badges.observeAsState()
|
||||||
|
val userBadge = userBadgeState?.user ?: return
|
||||||
|
|
||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.Bottom) {
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
@@ -445,6 +459,24 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userBadge.acceptedBadges?.let { note ->
|
||||||
|
(note.event as? BadgeProfilesEvent)?.let { event ->
|
||||||
|
FlowRow(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
|
||||||
|
event.badgeAwardEvents().forEach { badgeAwardEvent ->
|
||||||
|
val baseNote = LocalCache.notes[badgeAwardEvent]
|
||||||
|
if (baseNote != null) {
|
||||||
|
val badgeAwardState by baseNote.live().metadata.observeAsState()
|
||||||
|
val baseBadgeDefinition = badgeAwardState?.note?.replyTo?.firstOrNull()
|
||||||
|
|
||||||
|
if (baseBadgeDefinition != null) {
|
||||||
|
BadgeThumb(baseBadgeDefinition, navController, 50.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
user.info?.about?.let {
|
user.info?.about?.let {
|
||||||
Text(
|
Text(
|
||||||
it,
|
it,
|
||||||
@@ -454,6 +486,69 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BadgeThumb(
|
||||||
|
note: Note,
|
||||||
|
navController: NavController,
|
||||||
|
size: Dp,
|
||||||
|
pictureModifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
BadgeThumb(note, size, pictureModifier) {
|
||||||
|
navController.navigate("Note/${it.idHex}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BadgeThumb(
|
||||||
|
baseNote: Note,
|
||||||
|
size: Dp,
|
||||||
|
pictureModifier: Modifier = Modifier,
|
||||||
|
onClick: ((Note) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val noteState by baseNote.live().metadata.observeAsState()
|
||||||
|
val note = noteState?.note ?: return
|
||||||
|
|
||||||
|
val event = (note.event as? BadgeDefinitionEvent)
|
||||||
|
val image = event?.thumb() ?: event?.image()
|
||||||
|
|
||||||
|
val ctx = LocalContext.current.applicationContext
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.width(size)
|
||||||
|
.height(size)) {
|
||||||
|
if (image == null) {
|
||||||
|
Image(
|
||||||
|
painter = BitmapPainter(RoboHashCache.get(ctx, "ohnothisauthorisnotfound")),
|
||||||
|
contentDescription = stringResource(R.string.unknown_author),
|
||||||
|
modifier = pictureModifier
|
||||||
|
.fillMaxSize(1f)
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AsyncImage(
|
||||||
|
model = image,
|
||||||
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
|
placeholder = BitmapPainter(RoboHashCache.get(ctx, note.idHex)),
|
||||||
|
fallback = BitmapPainter(RoboHashCache.get(ctx, note.idHex)),
|
||||||
|
error = BitmapPainter(RoboHashCache.get(ctx, note.idHex)),
|
||||||
|
modifier = pictureModifier
|
||||||
|
.fillMaxSize(1f)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
.run {
|
||||||
|
if (onClick != null)
|
||||||
|
this.clickable(onClick = { onClick(note) } )
|
||||||
|
else
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun DrawBanner(baseUser: User) {
|
private fun DrawBanner(baseUser: User) {
|
||||||
@@ -471,7 +566,8 @@ private fun DrawBanner(baseUser: User) {
|
|||||||
contentScale = ContentScale.FillWidth,
|
contentScale = ContentScale.FillWidth,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(125.dp).combinedClickable(
|
.height(125.dp)
|
||||||
|
.combinedClickable(
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
clipboardManager.setText(AnnotatedString(banner))
|
clipboardManager.setText(AnnotatedString(banner))
|
||||||
|
@@ -187,4 +187,9 @@
|
|||||||
<string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string>
|
<string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string>
|
||||||
<string name="copy_my_secret_key">Copy my secret key</string>
|
<string name="copy_my_secret_key">Copy my secret key</string>
|
||||||
<string name="biometric_authentication_failed">Authentication failed</string>
|
<string name="biometric_authentication_failed">Authentication failed</string>
|
||||||
|
|
||||||
|
<string name="badge_created_by">"Created by %1$s"</string>
|
||||||
|
<string name="badge_award_image_for">"Badge award image for %1$s"</string>
|
||||||
|
<string name="new_badge_award_notif">You Received a new Badge Award</string>
|
||||||
|
<string name="award_granted_to">Badge award granted to</string>
|
||||||
</resources>
|
</resources>
|
Reference in New Issue
Block a user