mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2024-09-30 00:40:49 +00:00
Adds support for seeing NIP-89 recommendations on User's profile.
This commit is contained in:
parent
e28cd2212e
commit
00e7add001
@ -1,8 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
@ -13,7 +11,6 @@ import kotlinx.coroutines.*
|
|||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import nostr.postr.toNpub
|
import nostr.postr.toNpub
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -23,12 +20,6 @@ import java.time.format.DateTimeFormatter
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
object LocalCache {
|
object LocalCache {
|
||||||
val metadataParser by lazy {
|
|
||||||
jacksonObjectMapper()
|
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
|
||||||
.readerFor(UserMetadata::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
val antiSpam = AntiSpamFilter()
|
val antiSpam = AntiSpamFilter()
|
||||||
|
|
||||||
val users = ConcurrentHashMap<HexKey, User>(5000)
|
val users = ConcurrentHashMap<HexKey, User>(5000)
|
||||||
@ -130,18 +121,10 @@ object LocalCache {
|
|||||||
// new event
|
// new event
|
||||||
val oldUser = getOrCreateUser(event.pubKey)
|
val oldUser = getOrCreateUser(event.pubKey)
|
||||||
if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) {
|
if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) {
|
||||||
val newUser = try {
|
val newUserMetadata = event.contactMetaData()
|
||||||
metadataParser.readValue(
|
if (newUserMetadata != null) {
|
||||||
ByteArrayInputStream(event.content.toByteArray(Charsets.UTF_8)),
|
oldUser.updateUserInfo(newUserMetadata, event)
|
||||||
UserMetadata::class.java
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
Log.w("MT", "Content Parse Error ${e.localizedMessage} ${event.content}")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oldUser.updateUserInfo(newUser, event)
|
|
||||||
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
|
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}")
|
||||||
} else {
|
} else {
|
||||||
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
|
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
|
||||||
@ -357,6 +340,30 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(event: AppDefinitionEvent) {
|
||||||
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(event: AppRecommendationEvent) {
|
||||||
|
val note = getOrCreateAddressableNote(event.address())
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun consume(event: RecommendRelayEvent) {
|
fun consume(event: RecommendRelayEvent) {
|
||||||
// // Log.d("RR", event.toJson())
|
// // Log.d("RR", event.toJson())
|
||||||
@ -973,6 +980,8 @@ object LocalCache {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
is AppDefinitionEvent -> consume(event)
|
||||||
|
is AppRecommendationEvent -> consume(event)
|
||||||
is AudioTrackEvent -> consume(event)
|
is AudioTrackEvent -> consume(event)
|
||||||
is BadgeAwardEvent -> consume(event)
|
is BadgeAwardEvent -> consume(event)
|
||||||
is BadgeDefinitionEvent -> consume(event)
|
is BadgeDefinitionEvent -> consume(event)
|
||||||
|
@ -390,6 +390,10 @@ class UserMetadata {
|
|||||||
var updatedMetadataAt: Long = 0
|
var updatedMetadataAt: Long = 0
|
||||||
var latestMetadata: MetadataEvent? = null
|
var latestMetadata: MetadataEvent? = null
|
||||||
|
|
||||||
|
fun anyName(): String? {
|
||||||
|
return display_name ?: displayName ?: name ?: username
|
||||||
|
}
|
||||||
|
|
||||||
fun anyNameStartsWith(prefix: String): Boolean {
|
fun anyNameStartsWith(prefix: String): Boolean {
|
||||||
return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16)
|
return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16)
|
||||||
.any { it.startsWith(prefix, true) }
|
.any { it.startsWith(prefix, true) }
|
||||||
|
@ -81,9 +81,9 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
|||||||
TypedFilter(
|
TypedFilter(
|
||||||
types = COMMON_FEED_TYPES,
|
types = COMMON_FEED_TYPES,
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(BookmarkListEvent.kind, PeopleListEvent.kind),
|
kinds = listOf(BookmarkListEvent.kind, PeopleListEvent.kind, AppRecommendationEvent.kind),
|
||||||
authors = listOf(it.pubkeyHex),
|
authors = listOf(it.pubkeyHex),
|
||||||
limit = 1
|
limit = 100
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.UserMetadata
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class AppDefinitionEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
|
||||||
|
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||||
|
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||||
|
|
||||||
|
fun appMetaData() = try {
|
||||||
|
MetadataEvent.metadataParser.readValue(
|
||||||
|
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
|
||||||
|
UserMetadata::class.java
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Log.w("MT", "Content Parse Error ${e.localizedMessage} $content")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun supportedKinds() = tags.filter { it.size > 1 && it[0] == "k" }.mapNotNull {
|
||||||
|
runCatching { it[1].toInt() }.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun publishedAt() = tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 31990
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class AppRecommendationEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
|
||||||
|
fun recommendations() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
|
||||||
|
ATag.parse(it[1], it.getOrNull(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun forKind() = runCatching { dTag().toInt() }.getOrNull()
|
||||||
|
|
||||||
|
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||||
|
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 31989
|
||||||
|
}
|
||||||
|
}
|
@ -228,6 +228,8 @@ 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) {
|
||||||
|
AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
|
AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
BadgeAwardEvent.kind -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
|
BadgeAwardEvent.kind -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Immutable
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.UserMetadata
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class ContactMetaData(
|
|
||||||
val name: String,
|
|
||||||
val picture: String,
|
|
||||||
val about: String,
|
|
||||||
val nip05: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
abstract class IdentityClaim(
|
abstract class IdentityClaim(
|
||||||
var identity: String,
|
var identity: String,
|
||||||
var proof: String
|
var proof: String
|
||||||
@ -145,9 +140,13 @@ class MetadataEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun contactMetaData() = try {
|
fun contactMetaData() = try {
|
||||||
gson.fromJson(content, ContactMetaData::class.java)
|
metadataParser.readValue(
|
||||||
|
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
|
||||||
|
UserMetadata::class.java
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("MetadataEvent", "Can't parse $content", e)
|
e.printStackTrace()
|
||||||
|
Log.w("MT", "Content Parse Error ${e.localizedMessage} $content")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,8 +163,10 @@ class MetadataEvent(
|
|||||||
const val kind = 0
|
const val kind = 0
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
|
|
||||||
fun create(contactMetaData: ContactMetaData, identities: List<IdentityClaim>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
|
val metadataParser by lazy {
|
||||||
return create(gson.toJson(contactMetaData), identities, privateKey, createdAt = createdAt)
|
jacksonObjectMapper()
|
||||||
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
.readerFor(UserMetadata::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(contactMetaData: String, identities: List<IdentityClaim>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
|
fun create(contactMetaData: String, identities: List<IdentityClaim>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.model.AppRecommendationEvent
|
||||||
|
|
||||||
|
class UserProfileAppRecommendationsFeedFilter(val user: User) : FeedFilter<Note>() {
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val recommendations = LocalCache.addressables.values.filter {
|
||||||
|
(it.event as? AppRecommendationEvent)?.pubKey == user.pubkeyHex
|
||||||
|
}.mapNotNull {
|
||||||
|
(it.event as? AppRecommendationEvent)?.recommendations()
|
||||||
|
}.flatten()
|
||||||
|
.mapNotNull {
|
||||||
|
LocalCache.getOrCreateAddressableNote(it)
|
||||||
|
}.toSet().toList()
|
||||||
|
|
||||||
|
return recommendations
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.model.AppRecommendationEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
|
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ object UserProfileNewThreadFeedFilter : FeedFilter<Note>() {
|
|||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val longFormNotes = LocalCache.addressables.values
|
val longFormNotes = LocalCache.addressables.values
|
||||||
.filter { it.author == user && (it.event !is PeopleListEvent && it.event !is BookmarkListEvent) }
|
.filter { it.author == user && (it.event !is PeopleListEvent && it.event !is BookmarkListEvent && it.event !is AppRecommendationEvent) }
|
||||||
|
|
||||||
return user?.notes
|
return user?.notes
|
||||||
?.plus(longFormNotes)
|
?.plus(longFormNotes)
|
||||||
|
@ -40,7 +40,9 @@ import androidx.compose.material.Text
|
|||||||
import androidx.compose.material.darkColors
|
import androidx.compose.material.darkColors
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Bolt
|
import androidx.compose.material.icons.filled.Bolt
|
||||||
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
|
import androidx.compose.material.icons.filled.Link
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.PushPin
|
import androidx.compose.material.icons.filled.PushPin
|
||||||
import androidx.compose.material.lightColors
|
import androidx.compose.material.lightColors
|
||||||
@ -67,6 +69,7 @@ import androidx.compose.ui.graphics.luminance
|
|||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.painterResource
|
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
|
||||||
@ -88,6 +91,8 @@ import com.vitorpamplona.amethyst.model.Channel
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.model.UserMetadata
|
||||||
|
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
@ -110,6 +115,7 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
|
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
|
||||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
@ -120,10 +126,12 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
|||||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContent
|
import com.vitorpamplona.amethyst.ui.components.ZoomableContent
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableLocalImage
|
import com.vitorpamplona.amethyst.ui.components.ZoomableLocalImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableLocalVideo
|
import com.vitorpamplona.amethyst.ui.components.ZoomableLocalVideo
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlImage
|
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
|
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
|
||||||
import com.vitorpamplona.amethyst.ui.components.imageExtensions
|
import com.vitorpamplona.amethyst.ui.components.imageExtensions
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||||
@ -469,6 +477,10 @@ fun NormalNote(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (noteEvent) {
|
when (noteEvent) {
|
||||||
|
is AppDefinitionEvent -> {
|
||||||
|
RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
is ReactionEvent -> {
|
is ReactionEvent -> {
|
||||||
RenderReaction(baseNote, backgroundColor, accountViewModel, nav)
|
RenderReaction(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
@ -669,6 +681,190 @@ private fun RenderPoll(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun RenderAppDefinition(
|
||||||
|
note: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val noteEvent = note.event as? AppDefinitionEvent ?: return
|
||||||
|
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
val uri = LocalUriHandler.current
|
||||||
|
|
||||||
|
var metadata by remember {
|
||||||
|
mutableStateOf<UserMetadata?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = noteEvent) {
|
||||||
|
launch(Dispatchers.Default) {
|
||||||
|
metadata = noteEvent.appMetaData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata?.let {
|
||||||
|
Box {
|
||||||
|
if (!it.banner.isNullOrBlank()) {
|
||||||
|
var zoomImageDialogOpen by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = it.banner,
|
||||||
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(125.dp)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {
|
||||||
|
clipboardManager.setText(AnnotatedString(it.banner!!))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (zoomImageDialogOpen) {
|
||||||
|
ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.profile_banner),
|
||||||
|
contentDescription = stringResource(id = R.string.profile_banner),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(125.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
.padding(top = 75.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
var zoomImageDialogOpen by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box(Modifier.size(100.dp)) {
|
||||||
|
it.picture?.let {
|
||||||
|
AsyncImage(
|
||||||
|
model = it,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.border(
|
||||||
|
3.dp,
|
||||||
|
MaterialTheme.colors.background,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
.fillMaxSize()
|
||||||
|
.border(
|
||||||
|
3.dp,
|
||||||
|
MaterialTheme.colors.background,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { zoomImageDialogOpen = true },
|
||||||
|
onLongClick = {
|
||||||
|
clipboardManager.setText(AnnotatedString(it))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoomImageDialogOpen) {
|
||||||
|
ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(35.dp)
|
||||||
|
.padding(bottom = 3.dp)
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it.anyName()?.let {
|
||||||
|
Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 7.dp)) {
|
||||||
|
CreateTextWithEmoji(
|
||||||
|
text = it,
|
||||||
|
tags = remember { note.event?.tags() ?: emptyList() },
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 25.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = note.idDisplayNote(),
|
||||||
|
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(25.dp)
|
||||||
|
.padding(start = 5.dp),
|
||||||
|
onClick = { clipboardManager.setText(AnnotatedString(note.idDisplayNote())); }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ContentCopy,
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(15.dp),
|
||||||
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayNip05ProfileStatus(user)
|
||||||
|
|
||||||
|
val website = it.website
|
||||||
|
if (!website.isNullOrEmpty()) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
imageVector = Icons.Default.Link,
|
||||||
|
contentDescription = stringResource(R.string.website),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableText(
|
||||||
|
text = AnnotatedString(website.removePrefix("https://")),
|
||||||
|
onClick = { website.let { runCatching { uri.openUri(it) } } },
|
||||||
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
|
||||||
|
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it.about?.let {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
|
||||||
|
) {
|
||||||
|
TranslatableRichTextViewer(
|
||||||
|
content = it,
|
||||||
|
canPreview = false,
|
||||||
|
tags = remember { note.event?.tags() ?: emptyList() },
|
||||||
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderHighlight(
|
private fun RenderHighlight(
|
||||||
note: Note,
|
note: Note,
|
||||||
@ -2224,7 +2420,9 @@ fun NoteAuthorPicture(
|
|||||||
|
|
||||||
if (author == null) {
|
if (author == null) {
|
||||||
val nullModifier = remember {
|
val nullModifier = remember {
|
||||||
modifier.size(size).clip(shape = CircleShape)
|
modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
}
|
}
|
||||||
|
|
||||||
RobohashAsyncImage(
|
RobohashAsyncImage(
|
||||||
|
@ -33,7 +33,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -144,7 +143,6 @@ private fun WatchScrollToTop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FeedLoaded(
|
private fun FeedLoaded(
|
||||||
state: FeedState.Loaded,
|
state: FeedState.Loaded,
|
||||||
|
@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||||
@ -23,6 +24,7 @@ import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
||||||
@ -103,6 +105,14 @@ class NostrHomeRepliesFeedViewModel(val account: Account) : FeedViewModel(HomeCo
|
|||||||
class NostrBookmarkPublicFeedViewModel : FeedViewModel(BookmarkPublicFeedFilter)
|
class NostrBookmarkPublicFeedViewModel : FeedViewModel(BookmarkPublicFeedFilter)
|
||||||
class NostrBookmarkPrivateFeedViewModel : FeedViewModel(BookmarkPrivateFeedFilter)
|
class NostrBookmarkPrivateFeedViewModel : FeedViewModel(BookmarkPrivateFeedFilter)
|
||||||
|
|
||||||
|
class NostrUserAppRecommendationsFeedViewModel(val user: User) : FeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) {
|
||||||
|
class Factory(val user: User) : ViewModelProvider.Factory {
|
||||||
|
override fun <NostrUserAppRecommendationsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserAppRecommendationsFeedViewModel>): NostrUserAppRecommendationsFeedViewModel {
|
||||||
|
return NostrUserAppRecommendationsFeedViewModel(user) as NostrUserAppRecommendationsFeedViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||||
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
private val _feedContent = MutableStateFlow<FeedState>(FeedState.Loading)
|
||||||
|
@ -52,6 +52,7 @@ 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.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||||
@ -359,7 +360,14 @@ fun NoteMaster(
|
|||||||
} else if (noteEvent is AudioTrackEvent) {
|
} else if (noteEvent is AudioTrackEvent) {
|
||||||
AudioTrackHeader(noteEvent, accountViewModel, nav)
|
AudioTrackHeader(noteEvent, accountViewModel, nav)
|
||||||
} else if (noteEvent is PinListEvent) {
|
} else if (noteEvent is PinListEvent) {
|
||||||
PinListHeader(baseNote, MaterialTheme.colors.background, accountViewModel, nav)
|
PinListHeader(
|
||||||
|
baseNote,
|
||||||
|
MaterialTheme.colors.background,
|
||||||
|
accountViewModel,
|
||||||
|
nav
|
||||||
|
)
|
||||||
|
} else if (noteEvent is AppDefinitionEvent) {
|
||||||
|
RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||||
} else if (noteEvent is HighlightEvent) {
|
} else if (noteEvent is HighlightEvent) {
|
||||||
DisplayHighlight(
|
DisplayHighlight(
|
||||||
noteEvent.quote(),
|
noteEvent.quote(),
|
||||||
|
@ -4,6 +4,8 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.gestures.scrollBy
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@ -56,6 +58,7 @@ 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.NostrUserProfileDataSource
|
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.IdentityClaim
|
import com.vitorpamplona.amethyst.service.model.IdentityClaim
|
||||||
@ -78,7 +81,9 @@ 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.navigation.ShowQRDialog
|
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
|
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowersUserFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowersUserFeedViewModel
|
||||||
@ -141,10 +146,18 @@ fun PrepareViewModels(baseUser: User, accountViewModel: AccountViewModel, nav: (
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val appRecommendations: NostrUserAppRecommendationsFeedViewModel = viewModel(
|
||||||
|
key = baseUser.pubkeyHex + "UserAppRecommendationsFeedViewModel",
|
||||||
|
factory = NostrUserAppRecommendationsFeedViewModel.Factory(
|
||||||
|
baseUser
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
ProfileScreen(
|
ProfileScreen(
|
||||||
baseUser = baseUser,
|
baseUser = baseUser,
|
||||||
followsFeedViewModel,
|
followsFeedViewModel,
|
||||||
followersFeedViewModel,
|
followersFeedViewModel,
|
||||||
|
appRecommendations,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav
|
nav = nav
|
||||||
)
|
)
|
||||||
@ -156,6 +169,7 @@ fun ProfileScreen(
|
|||||||
baseUser: User,
|
baseUser: User,
|
||||||
followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel,
|
followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel,
|
||||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||||
|
appRecommendations: NostrUserAppRecommendationsFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
nav: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
@ -236,7 +250,7 @@ fun ProfileScreen(
|
|||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding()) {
|
Column(modifier = Modifier.padding()) {
|
||||||
ProfileHeader(baseUser, nav, accountViewModel)
|
ProfileHeader(baseUser, appRecommendations, nav, accountViewModel)
|
||||||
ScrollableTabRow(
|
ScrollableTabRow(
|
||||||
backgroundColor = MaterialTheme.colors.background,
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
@ -279,9 +293,9 @@ fun ProfileScreen(
|
|||||||
2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
|
2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
|
||||||
3 -> TabFollows(baseUser, followersFeedViewModel, accountViewModel, nav)
|
3 -> TabFollows(baseUser, followersFeedViewModel, accountViewModel, nav)
|
||||||
4 -> TabReceivedZaps(baseUser, accountViewModel, nav)
|
4 -> TabReceivedZaps(baseUser, accountViewModel, nav)
|
||||||
5 -> TabBookmarks(baseUser, accountViewModel, nav)
|
6 -> TabBookmarks(baseUser, accountViewModel, nav)
|
||||||
6 -> TabReports(baseUser, accountViewModel, nav)
|
7 -> TabReports(baseUser, accountViewModel, nav)
|
||||||
7 -> TabRelays(baseUser, accountViewModel)
|
8 -> TabRelays(baseUser, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -397,6 +411,7 @@ private fun FollowTabHeader(baseUser: User) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ProfileHeader(
|
private fun ProfileHeader(
|
||||||
baseUser: User,
|
baseUser: User,
|
||||||
|
appRecommendations: NostrUserAppRecommendationsFeedViewModel,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
) {
|
) {
|
||||||
@ -486,7 +501,7 @@ private fun ProfileHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawAdditionalInfo(baseUser, accountViewModel, nav)
|
DrawAdditionalInfo(baseUser, appRecommendations, accountViewModel, nav)
|
||||||
|
|
||||||
Divider(modifier = Modifier.padding(top = 6.dp))
|
Divider(modifier = Modifier.padding(top = 6.dp))
|
||||||
}
|
}
|
||||||
@ -555,7 +570,12 @@ private fun ProfileActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DrawAdditionalInfo(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
private fun DrawAdditionalInfo(
|
||||||
|
baseUser: User,
|
||||||
|
appRecommendations: NostrUserAppRecommendationsFeedViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
val userState by baseUser.live().metadata.observeAsState()
|
val userState by baseUser.live().metadata.observeAsState()
|
||||||
val user = remember(userState) { userState?.user } ?: return
|
val user = remember(userState) { userState?.user } ?: return
|
||||||
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
||||||
@ -704,6 +724,8 @@ private fun DrawAdditionalInfo(baseUser: User, accountViewModel: AccountViewMode
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisplayAppRecommendations(appRecommendations, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -784,6 +806,81 @@ private fun DisplayLNAddress(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
private fun DisplayAppRecommendations(
|
||||||
|
appRecommendations: NostrUserAppRecommendationsFeedViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val feedState by appRecommendations.feedContent.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = Unit) {
|
||||||
|
appRecommendations.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
Crossfade(
|
||||||
|
targetState = feedState,
|
||||||
|
animationSpec = tween(durationMillis = 100)
|
||||||
|
) { state ->
|
||||||
|
when (state) {
|
||||||
|
is FeedState.Loaded -> {
|
||||||
|
Column() {
|
||||||
|
Text(stringResource(id = R.string.recommended_apps))
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp)
|
||||||
|
) {
|
||||||
|
state.feed.value.forEach { app ->
|
||||||
|
WatchApp(app, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WatchApp(baseApp: Note, nav: (String) -> Unit) {
|
||||||
|
val appState by baseApp.live().metadata.observeAsState()
|
||||||
|
|
||||||
|
var appLogo by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = appState) {
|
||||||
|
launch(Dispatchers.Default) {
|
||||||
|
val newAppLogo = (appState?.note?.event as? AppDefinitionEvent)?.appMetaData()?.picture?.ifBlank { null }
|
||||||
|
if (newAppLogo != appLogo) {
|
||||||
|
appLogo = newAppLogo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appLogo?.let {
|
||||||
|
Box(
|
||||||
|
remember {
|
||||||
|
Modifier
|
||||||
|
.size(35.dp)
|
||||||
|
.clickable {
|
||||||
|
nav("Note/${baseApp.idHex}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = appLogo,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = remember {
|
||||||
|
Modifier
|
||||||
|
.size(35.dp)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
private fun DisplayBadges(
|
private fun DisplayBadges(
|
||||||
@ -906,7 +1003,7 @@ fun BadgeThumb(
|
|||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun DrawBanner(baseUser: User) {
|
public fun DrawBanner(baseUser: User) {
|
||||||
val userState by baseUser.live().metadata.observeAsState()
|
val userState by baseUser.live().metadata.observeAsState()
|
||||||
val banner = remember(userState) { userState?.user?.info?.banner }
|
val banner = remember(userState) { userState?.user?.info?.banner }
|
||||||
|
|
||||||
@ -922,12 +1019,11 @@ private fun DrawBanner(baseUser: User) {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(125.dp)
|
.height(125.dp)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {},
|
onClick = { zoomImageDialogOpen = true },
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
clipboardManager.setText(AnnotatedString(banner))
|
clipboardManager.setText(AnnotatedString(banner))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.clickable { zoomImageDialogOpen = true }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (zoomImageDialogOpen) {
|
if (zoomImageDialogOpen) {
|
||||||
|
@ -410,4 +410,6 @@
|
|||||||
<string name="content_warning_hide_all_sensitive_content">Always hide sensitive content</string>
|
<string name="content_warning_hide_all_sensitive_content">Always hide sensitive content</string>
|
||||||
<string name="content_warning_show_all_sensitive_content">Always show sensitive content</string>
|
<string name="content_warning_show_all_sensitive_content">Always show sensitive content</string>
|
||||||
<string name="content_warning_see_warnings">Always show content warnings</string>
|
<string name="content_warning_see_warnings">Always show content warnings</string>
|
||||||
|
|
||||||
|
<string name="recommended_apps">Recommends: </string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user