mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Refactors the old NIP-05 code on Quartz
New Caching system for User metadata New Caching system for NIP-05 verifications
This commit is contained in:
+41
-6
@@ -28,6 +28,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.actions.buildAnnotatedStringWithUrlHighlighting
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -77,8 +78,19 @@ class UrlUserTagTransformationTest {
|
||||
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
|
||||
.toHexKey(),
|
||||
)
|
||||
user.info = UserMetadata()
|
||||
user.info?.displayName = "Vitor Pamplona"
|
||||
user.metadata().newMetadata(
|
||||
UserMetadata().also {
|
||||
it.displayName = "Vitor Pamplona"
|
||||
},
|
||||
MetadataEvent(
|
||||
id = "",
|
||||
pubKey = "",
|
||||
createdAt = 0,
|
||||
tags = emptyArray(),
|
||||
content = "",
|
||||
sig = "",
|
||||
),
|
||||
)
|
||||
|
||||
val original = "@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"
|
||||
|
||||
@@ -185,8 +197,19 @@ class UrlUserTagTransformationTest {
|
||||
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
|
||||
.toHexKey(),
|
||||
)
|
||||
user.info = UserMetadata()
|
||||
user.info?.displayName = "Vitor Pamplona"
|
||||
user.metadata().newMetadata(
|
||||
UserMetadata().also {
|
||||
it.displayName = "Vitor Pamplona"
|
||||
},
|
||||
MetadataEvent(
|
||||
id = "",
|
||||
pubKey = "",
|
||||
createdAt = 0,
|
||||
tags = emptyArray(),
|
||||
content = "",
|
||||
sig = "",
|
||||
),
|
||||
)
|
||||
|
||||
val transformedText =
|
||||
buildAnnotatedStringWithUrlHighlighting(
|
||||
@@ -224,8 +247,20 @@ class UrlUserTagTransformationTest {
|
||||
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
|
||||
.toHexKey(),
|
||||
)
|
||||
user.info = UserMetadata()
|
||||
user.info?.displayName = "Vitor Pamplona"
|
||||
|
||||
user.metadata().newMetadata(
|
||||
UserMetadata().also {
|
||||
it.displayName = "Vitor Pamplona"
|
||||
},
|
||||
MetadataEvent(
|
||||
id = "",
|
||||
pubKey = "",
|
||||
createdAt = 0,
|
||||
tags = emptyArray(),
|
||||
content = "",
|
||||
sig = "",
|
||||
),
|
||||
)
|
||||
|
||||
val transformedText =
|
||||
buildAnnotatedStringWithUrlHighlighting(
|
||||
|
||||
@@ -65,6 +65,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.stats.RelayReqStats
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.OkHttpNip05Fetcher
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -137,6 +139,10 @@ class AppModules(
|
||||
// Offers easy methods to know when connections are happening through Tor or not
|
||||
val roleBasedHttpClientBuilder = RoleBasedHttpClientBuilder(okHttpClients, torPrefs.value)
|
||||
|
||||
// Custom fetcher that considers tor settings and avoids forwarding.
|
||||
val nip05Fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05)
|
||||
val nip05Client = Nip05Client(nip05Fetcher)
|
||||
|
||||
// Application-wide block height request cache
|
||||
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ fun debugState(context: Context) {
|
||||
"Users: " +
|
||||
LocalCache.users.filter { _, it -> it.flowSet != null }.size +
|
||||
" / " +
|
||||
LocalCache.users.filter { _, it -> it.latestMetadata != null }.size +
|
||||
LocalCache.users.filter { _, it -> it.metadataOrNull() != null }.size +
|
||||
" / " +
|
||||
LocalCache.users.size(),
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.hashtags.Skull
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Tunestr
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Weed
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Zap
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
|
||||
import com.vitorpamplona.amethyst.ui.components.HashTag
|
||||
@@ -52,7 +53,6 @@ import com.vitorpamplona.amethyst.ui.components.RenderRegular
|
||||
import com.vitorpamplona.amethyst.ui.components.RenderTextParagraph
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
@@ -488,20 +488,25 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
// new event
|
||||
val oldUser = getOrCreateUser(event.pubKey)
|
||||
val currentMetadata = oldUser.latestMetadata
|
||||
consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
if (currentMetadata == null || event.createdAt > currentMetadata.createdAt) {
|
||||
oldUser.latestMetadata = event
|
||||
val user = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (user.metadata().shouldUpdateWith(event)) {
|
||||
val newUserMetadata = event.contactMetaData()
|
||||
if (newUserMetadata != null && (wasVerified || justVerify(event))) {
|
||||
oldUser.updateUserInfo(newUserMetadata, event)
|
||||
if (relay != null) {
|
||||
oldUser.addRelayBeingUsed(relay, event.createdAt)
|
||||
if (!relay.isLocalHost()) {
|
||||
oldUser.latestMetadataRelay = relay
|
||||
val fallbackRelay =
|
||||
relay?.isLocalHost()?.let {
|
||||
if (!it) {
|
||||
relay
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
user.updateUserInfo(newUserMetadata, event, fallbackRelay)
|
||||
if (relay != null) {
|
||||
user.addRelayBeingUsed(relay, event.createdAt)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -2106,7 +2111,9 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
note.event is ReactionEvent ||
|
||||
note.event is LnZapEvent ||
|
||||
note.event is LnZapRequestEvent ||
|
||||
note.event is FileHeaderEvent
|
||||
note.event is FileHeaderEvent ||
|
||||
note.event is MetadataEvent ||
|
||||
note.event is ContactListEvent
|
||||
)
|
||||
|
||||
fun findNotesStartingWith(
|
||||
|
||||
+11
-6
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model.nip01UserMetadata
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
@@ -30,7 +29,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UserMetadataState(
|
||||
@@ -42,11 +40,16 @@ class UserMetadataState(
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val user = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val note = cache.getOrCreateAddressableNote(getMetadataEventAddress())
|
||||
|
||||
fun getMetadataEventAddress() = MetadataEvent.createAddress(signer.pubKey)
|
||||
|
||||
// fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey)
|
||||
|
||||
fun getUserMetadataFlow(): StateFlow<UserState> = user.flow().metadata.stateFlow
|
||||
fun getUserMetadataFlow() = note.flow().metadata.stateFlow
|
||||
|
||||
fun getUserMetadataEvent(): MetadataEvent? = user.latestMetadata
|
||||
fun getUserMetadataEvent(): MetadataEvent? = note.event as MetadataEvent?
|
||||
|
||||
suspend fun sendNewUserMetadata(
|
||||
name: String? = null,
|
||||
@@ -116,8 +119,10 @@ class UserMetadataState(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Kind 0 Collector Start")
|
||||
getUserMetadataFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 0 ${it.user.toBestDisplayName()}")
|
||||
settings.updateUserMetadata(it.user.latestMetadata)
|
||||
Log.d("AccountRegisterObservers", "Updating Kind 0 ${user.toBestDisplayName()}")
|
||||
(it.note.event as? MetadataEvent)?.let {
|
||||
settings.updateUserMetadata(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.util.LruCache
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
|
||||
object CachedRichTextParser {
|
||||
private val richTextCache = LruCache<Int, RichTextViewerState>(50)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
|
||||
fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2
|
||||
|
||||
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
|
||||
object Nip05NostrAddressVerifier {
|
||||
suspend fun fetchNip05Json(
|
||||
nip05: String,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val url = Nip05().assembleUrl(nip05)
|
||||
|
||||
if (url == null) {
|
||||
onError("Could not assemble url from Nip05: \"${nip05}\". Check the user's setup")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
try {
|
||||
val request = Request.Builder().url(url).build()
|
||||
|
||||
// Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint.
|
||||
val client = okHttpClient(url).newBuilder().followRedirects(false).build()
|
||||
|
||||
client.newCall(request).executeAsync().use { response ->
|
||||
withContext(Dispatchers.IO) {
|
||||
if (response.isSuccessful) {
|
||||
onSuccess(response.body.string())
|
||||
} else {
|
||||
onError(
|
||||
"Could not resolve $nip05. Error: ${response.code}. Check if the server is up and if the address $nip05 is correct",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError("Could not resolve NIP-05 $nip05 as URL $url: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun verifyNip05(
|
||||
nip05: String,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
) {
|
||||
fetchNip05Json(
|
||||
nip05,
|
||||
okHttpClient,
|
||||
onSuccess = {
|
||||
Nip05().parseHexKeyFor(nip05, it.lowercase()).fold(
|
||||
onSuccess = { hexKey ->
|
||||
if (hexKey == null) {
|
||||
onError("Username not found in the NIP05 JSON [$nip05]")
|
||||
} else {
|
||||
onSuccess(hexKey)
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
onError("Error Parsing JSON from NIP-05 address: $nip05." + (it.message ?: it.localizedMessage ?: it.javaClass.simpleName))
|
||||
},
|
||||
)
|
||||
},
|
||||
onError = onError,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ class ZapPaymentHandler(
|
||||
is ZapSplitSetup -> {
|
||||
val user = LocalCache.checkGetOrCreateUser(setup.pubKeyHex)
|
||||
UnverifiedZapSplitSetup(
|
||||
lnAddress = user?.info?.lnAddress(),
|
||||
lnAddress = user?.lnAddress(),
|
||||
weight = setup.weight,
|
||||
relay = setup.relay,
|
||||
user = user,
|
||||
@@ -111,7 +111,7 @@ class ZapPaymentHandler(
|
||||
} else if (noteEvent is LiveActivitiesEvent && noteEvent.hasHost()) {
|
||||
noteEvent.hosts().map {
|
||||
val user = LocalCache.checkGetOrCreateUser(it.pubKey)
|
||||
val lnAddress = user?.info?.lnAddress()
|
||||
val lnAddress = user?.lnAddress()
|
||||
UnverifiedZapSplitSetup(lnAddress, relay = it.relayHint, user = user)
|
||||
}
|
||||
} else if (noteEvent is AppDefinitionEvent) {
|
||||
@@ -119,11 +119,16 @@ class ZapPaymentHandler(
|
||||
if (appLud16 != null) {
|
||||
listOf(UnverifiedZapSplitSetup(appLud16))
|
||||
} else {
|
||||
val lud16 = note.author?.info?.lnAddress()
|
||||
val lud16 =
|
||||
note.author?.lnAddress()
|
||||
listOf(UnverifiedZapSplitSetup(lud16))
|
||||
}
|
||||
} else {
|
||||
listOf(UnverifiedZapSplitSetup(note.author?.info?.lnAddress()))
|
||||
listOf(
|
||||
UnverifiedZapSplitSetup(
|
||||
note.author?.lnAddress(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (showErrorIfNoLnAddress) {
|
||||
|
||||
+1
-1
@@ -39,6 +39,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
|
||||
@@ -46,7 +47,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
|
||||
@Composable
|
||||
fun NotifyRequestDialog(
|
||||
|
||||
+48
-91
@@ -26,6 +26,7 @@ import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip01Core.UserInfo
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
@@ -34,7 +35,6 @@ import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
@@ -46,27 +46,13 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Composable
|
||||
fun observeUser(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<UserState?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return user
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeUserName(
|
||||
@@ -79,38 +65,17 @@ fun observeUserName(
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.user.toBestDisplayName() }
|
||||
.distinctUntilChanged()
|
||||
.metadata()
|
||||
.flow
|
||||
.map {
|
||||
it?.info?.bestName() ?: user.pubkeyDisplayHex()
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle(user.toBestDisplayName())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeUserNip05(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<String?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.user.info?.nip05 }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
return flow.collectAsStateWithLifecycle(user.info?.nip05)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeUserAboutMe(
|
||||
@@ -123,14 +88,19 @@ fun observeUserAboutMe(
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.user.info?.about ?: "" }
|
||||
.distinctUntilChanged()
|
||||
user.metadata().flow.map {
|
||||
it?.info?.about ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(user.info?.about ?: "")
|
||||
return flow.collectAsStateWithLifecycle(
|
||||
user
|
||||
.metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.about ?: "",
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -138,23 +108,15 @@ fun observeUserAboutMe(
|
||||
fun observeUserInfo(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<UserMetadata?> {
|
||||
): State<UserInfo?> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.user.info }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(user.info)
|
||||
return user.metadata().flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeUserBanner(
|
||||
@@ -168,15 +130,24 @@ fun observeUserBanner(
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.user.info?.banner }
|
||||
.distinctUntilChanged()
|
||||
.metadata()
|
||||
.flow
|
||||
.map {
|
||||
it?.info?.banner
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(user.info?.banner)
|
||||
return flow.collectAsStateWithLifecycle(
|
||||
user
|
||||
.metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.banner,
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeUserPicture(
|
||||
@@ -190,35 +161,21 @@ fun observeUserPicture(
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.user.info?.picture }
|
||||
.distinctUntilChanged()
|
||||
.metadata()
|
||||
.flow
|
||||
.map {
|
||||
it?.info?.picture
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(user.info?.picture)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeUserShortName(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<String> {
|
||||
// Subscribe in the relay for changes in the metadata of this user.
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
remember(user) {
|
||||
user
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.mapLatest { it.user.toBestShortFirstName() }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(user.toBestShortFirstName())
|
||||
return flow.collectAsStateWithLifecycle(
|
||||
user
|
||||
.metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.picture,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ fun filterUserMetadataForKey(
|
||||
|
||||
users.forEach { user ->
|
||||
val time = since.since(user)?.get(relay)?.time
|
||||
if (time == null || user.latestMetadata == null) {
|
||||
if (time == null || user.metadataOrNull()?.flow?.value == null) {
|
||||
firstTimers.add(user.pubkeyHex)
|
||||
} else {
|
||||
updates.add(user.pubkeyHex)
|
||||
|
||||
@@ -51,6 +51,7 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -76,6 +77,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.BechLink
|
||||
@@ -273,27 +275,30 @@ fun EditPostView(
|
||||
}
|
||||
}
|
||||
|
||||
val user = postViewModel.account?.userProfile()
|
||||
val lud16 = user?.info?.lnAddress()
|
||||
if (postViewModel.wantsInvoice) {
|
||||
val user = postViewModel.account.userProfile()
|
||||
val info by observeUserInfo(user, accountViewModel)
|
||||
val lud16 = info?.info?.lnAddress()
|
||||
|
||||
if (lud16 != null && postViewModel.wantsInvoice) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
InvoiceRequest(
|
||||
lud16,
|
||||
user,
|
||||
accountViewModel,
|
||||
stringRes(id = R.string.lightning_invoice),
|
||||
stringRes(id = R.string.lightning_create_and_add_invoice),
|
||||
onNewInvoice = {
|
||||
postViewModel.message =
|
||||
TextFieldValue(postViewModel.message.text + "\n\n" + it)
|
||||
postViewModel.wantsInvoice = false
|
||||
},
|
||||
onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
|
||||
)
|
||||
if (lud16 != null) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
|
||||
) {
|
||||
InvoiceRequest(
|
||||
lud16,
|
||||
user,
|
||||
accountViewModel,
|
||||
stringRes(id = R.string.lightning_invoice),
|
||||
stringRes(id = R.string.lightning_create_and_add_invoice),
|
||||
onNewInvoice = {
|
||||
postViewModel.message =
|
||||
TextFieldValue(postViewModel.message.text + "\n\n" + it)
|
||||
postViewModel.wantsInvoice = false
|
||||
},
|
||||
onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ open class EditPostViewModel : ViewModel() {
|
||||
edit: Note,
|
||||
versionLookingAt: Note?,
|
||||
) {
|
||||
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canAddInvoice = accountViewModel.userProfile().lnAddress() != null
|
||||
multiOrchestrator = null
|
||||
|
||||
message = TextFieldValue(versionLookingAt?.event?.content ?: edit.event?.content ?: "")
|
||||
|
||||
+16
-17
@@ -40,7 +40,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class NewUserMetadataViewModel : ViewModel() {
|
||||
@@ -73,28 +72,28 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun load() {
|
||||
account.userProfile().let {
|
||||
name.value = it.info?.name ?: ""
|
||||
displayName.value = it.info?.displayName ?: ""
|
||||
about.value = it.info?.about ?: ""
|
||||
picture.value = it.info?.picture ?: ""
|
||||
banner.value = it.info?.banner ?: ""
|
||||
website.value = it.info?.website ?: ""
|
||||
pronouns.value = it.info?.pronouns ?: ""
|
||||
nip05.value = it.info?.nip05 ?: ""
|
||||
lnAddress.value = it.info?.lud16 ?: ""
|
||||
lnURL.value = it.info?.lud06 ?: ""
|
||||
account.userProfile().metadataOrNull()?.flow?.value?.let {
|
||||
name.value = it.info.name ?: ""
|
||||
displayName.value = it.info.displayName ?: ""
|
||||
about.value = it.info.about ?: ""
|
||||
picture.value = it.info.picture ?: ""
|
||||
banner.value = it.info.banner ?: ""
|
||||
website.value = it.info.website ?: ""
|
||||
pronouns.value = it.info.pronouns ?: ""
|
||||
nip05.value = it.info.nip05 ?: ""
|
||||
lnAddress.value = it.info.lud16 ?: ""
|
||||
lnURL.value = it.info.lud06 ?: ""
|
||||
|
||||
twitter.value = ""
|
||||
github.value = ""
|
||||
mastodon.value = ""
|
||||
|
||||
// TODO: Validate Telegram input, somehow.
|
||||
it.latestMetadata?.identityClaims()?.forEach {
|
||||
when (it) {
|
||||
is TwitterIdentity -> twitter.value = it.toProofUrl()
|
||||
is GitHubIdentity -> github.value = it.toProofUrl()
|
||||
is MastodonIdentity -> mastodon.value = it.toProofUrl()
|
||||
it.identities.forEach { identity ->
|
||||
when (identity) {
|
||||
is TwitterIdentity -> twitter.value = identity.toProofUrl()
|
||||
is GitHubIdentity -> github.value = identity.toProofUrl()
|
||||
is MastodonIdentity -> mastodon.value = identity.toProofUrl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.R.attr.maxLines
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
@@ -54,6 +55,8 @@ import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
@@ -63,10 +66,8 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.njumpLink
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
|
||||
@@ -284,7 +285,7 @@ fun RenderUserAsClickableText(
|
||||
val userState by observeUserInfo(baseUser, accountViewModel)
|
||||
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = "@" + (userState?.bestName() ?: baseUser.pubkeyDisplayHex()),
|
||||
clickablePart = "@" + (userState?.info?.bestName() ?: baseUser.pubkeyDisplayHex()),
|
||||
suffix = additionalChars?.ifBlank { null },
|
||||
maxLines = 1,
|
||||
route = remember(baseUser) { routeFor(baseUser) },
|
||||
|
||||
+1
-1
@@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.richtext.ExpandableTextCutOffCalculator
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.getGradient
|
||||
@@ -48,7 +49,6 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
|
||||
object ShowFullTextCache {
|
||||
val cache = LruCache<String, Boolean>(10)
|
||||
|
||||
@@ -62,6 +62,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.richtext.Base64Segment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BechSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.CashuSegment
|
||||
@@ -107,8 +109,6 @@ import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.inlinePlaceholder
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -848,7 +848,7 @@ private fun DisplayUserFromTag(
|
||||
CrossfadeIfEnabled(targetState = meta, label = "DisplayUserFromTag", accountViewModel = accountViewModel) {
|
||||
Row {
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = remember(meta) { it?.bestName() ?: baseUser.pubkeyDisplayHex() },
|
||||
clickablePart = remember(meta) { it?.info?.bestName() ?: baseUser.pubkeyDisplayHex() },
|
||||
maxLines = 1,
|
||||
route = remember(baseUser) { routeFor(baseUser) },
|
||||
nav = nav,
|
||||
|
||||
+2
-2
@@ -39,6 +39,8 @@ import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.preview.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
@@ -51,8 +53,6 @@ import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
|
||||
import com.vitorpamplona.amethyst.ui.uriToRoute
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetasByUrl
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ fun ErrorRow(
|
||||
},
|
||||
) {
|
||||
val descriptor =
|
||||
it.info?.bestName()?.let {
|
||||
it.metadataOrNull()?.flow?.value?.info?.bestName()?.let {
|
||||
stringRes(R.string.error_dialog_talk_to_user_name, it)
|
||||
} ?: stringRes(R.string.error_dialog_talk_to_user)
|
||||
|
||||
|
||||
+3
-4
@@ -60,7 +60,6 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
|
||||
@@ -209,11 +208,11 @@ private fun AccountPicture(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val profilePicture by observeUserPicture(user, accountViewModel)
|
||||
val userInfo by observeUserInfo(user, accountViewModel)
|
||||
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = user.pubkeyHex,
|
||||
model = profilePicture,
|
||||
model = userInfo?.info?.profilePicture(),
|
||||
contentDescription = stringRes(R.string.profile_image),
|
||||
modifier = AccountPictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
@@ -230,7 +229,7 @@ private fun AccountName(
|
||||
val info by observeUserInfo(user, accountViewModel)
|
||||
|
||||
info?.let {
|
||||
it.bestName()?.let { name ->
|
||||
it.info.bestName()?.let { name ->
|
||||
CreateTextWithEmoji(
|
||||
text = name,
|
||||
tags = it.tags,
|
||||
|
||||
+4
-4
@@ -91,6 +91,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
@@ -122,7 +123,6 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.profileContentHeaderModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
@@ -196,9 +196,9 @@ fun ProfileContent(
|
||||
|
||||
ProfileContentTemplate(
|
||||
profilePubHex = baseAccountUser.pubkeyHex,
|
||||
profileBanner = userInfo?.banner,
|
||||
profilePicture = userInfo?.profilePicture(),
|
||||
bestDisplayName = userInfo?.bestName(),
|
||||
profileBanner = userInfo?.info?.banner,
|
||||
profilePicture = userInfo?.info?.profilePicture(),
|
||||
bestDisplayName = userInfo?.info?.bestName(),
|
||||
tags = userInfo?.tags,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
|
||||
@@ -63,6 +63,7 @@ import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -98,7 +99,6 @@ import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifierWithPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
+105
-135
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -34,10 +34,8 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -46,78 +44,45 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Tunestr
|
||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
|
||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05VerifState
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNip05
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchAuthor
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.nip05
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.firstTaggedAddress
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun nip05VerificationAsAState(
|
||||
userMetadata: UserMetadata,
|
||||
pubkeyHex: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
): MutableState<Boolean?> {
|
||||
val nip05Verified =
|
||||
remember(userMetadata.nip05) {
|
||||
// starts with null if must verify or already filled in if verified in the last hour
|
||||
val default =
|
||||
if ((userMetadata.nip05LastVerificationTime ?: 0) > TimeUtils.oneHourAgo()) {
|
||||
userMetadata.nip05Verified
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
mutableStateOf(default)
|
||||
}
|
||||
|
||||
if (nip05Verified.value == null) {
|
||||
LaunchedEffect(key1 = userMetadata.nip05) {
|
||||
accountViewModel.verifyNip05(userMetadata, pubkeyHex) { newVerificationStatus ->
|
||||
if (nip05Verified.value != newVerificationStatus) {
|
||||
nip05Verified.value = newVerificationStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nip05Verified
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveDisplayNip05Status(
|
||||
baseNote: Note,
|
||||
@@ -135,42 +100,42 @@ fun ObserveDisplayNip05Status(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val nip05 by observeUserNip05(baseUser, accountViewModel)
|
||||
val statuses by observeUserStatuses(baseUser, accountViewModel)
|
||||
val nip05State by baseUser.nip05State().flow.collectAsStateWithLifecycle()
|
||||
|
||||
CrossfadeIfEnabled(
|
||||
targetState = nip05,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
VerifyAndDisplayNIP05OrStatusLine(
|
||||
it,
|
||||
statuses,
|
||||
baseUser,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
VerifyAndDisplayNIP05OrStatusLine(
|
||||
nip05State,
|
||||
statuses,
|
||||
baseUser,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerifyAndDisplayNIP05OrStatusLine(
|
||||
nip05: String?,
|
||||
nip05State: Nip05State,
|
||||
statuses: ImmutableList<AddressableNote>,
|
||||
baseUser: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (nip05 != null) {
|
||||
val nip05Verified =
|
||||
nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex, accountViewModel)
|
||||
if (nip05State is Nip05State.Exists) {
|
||||
val nip05VerifState by nip05State.verificationState.collectAsStateWithLifecycle()
|
||||
|
||||
if (nip05Verified.value != true) {
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
} else if (!statuses.isEmpty()) {
|
||||
if (nip05VerifState.isExpired()) {
|
||||
LaunchedEffect(key1 = nip05VerifState) {
|
||||
accountViewModel.runOnIO {
|
||||
nip05State.checkAndUpdate(Amethyst.instance.nip05Client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nip05VerifState is Nip05VerifState.Verified && !statuses.isEmpty()) {
|
||||
ObserveRotateStatuses(statuses, accountViewModel, nav)
|
||||
} else {
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
DisplayNIP05(nip05State, nip05VerifState, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
if (!statuses.isEmpty()) {
|
||||
@@ -358,25 +323,15 @@ fun DisplayStatusInner(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayNIP05(
|
||||
nip05: String,
|
||||
nip05Verified: MutableState<Boolean?>,
|
||||
fun ObserveAndDisplayNIP05(
|
||||
nip05State: Nip05State.Exists,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val uri = LocalUriHandler.current
|
||||
val (user, domain) =
|
||||
remember(nip05) {
|
||||
val parts = nip05.split("@")
|
||||
if (parts.size == 1) {
|
||||
listOf("_", parts[0])
|
||||
} else {
|
||||
listOf(parts[0], parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
if (user != "_") {
|
||||
if (nip05State.nip05.name != "_") {
|
||||
Text(
|
||||
text = remember(nip05) { AnnotatedString(user) },
|
||||
text = remember(nip05State) { AnnotatedString(nip05State.nip05.name) },
|
||||
fontSize = Font14SP,
|
||||
color = MaterialTheme.colorScheme.nip05,
|
||||
maxLines = 1,
|
||||
@@ -384,42 +339,104 @@ fun DisplayNIP05(
|
||||
)
|
||||
}
|
||||
|
||||
NIP05VerifiedSymbol(nip05Verified, 1, NIP05IconSize, accountViewModel)
|
||||
ObserveAndRenderNIP05VerifiedSymbol(nip05State, 1, NIP05IconSize, accountViewModel)
|
||||
|
||||
ClickableTextPrimary(
|
||||
text = domain,
|
||||
onClick = { runCatching { uri.openUri("https://$domain") } },
|
||||
style =
|
||||
LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP),
|
||||
text = nip05State.nip05.domain,
|
||||
onClick = { runCatching { uri.openUri("https://${nip05State.nip05.domain}") } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NIP05VerifiedSymbol(
|
||||
nip05Verified: MutableState<Boolean?>,
|
||||
fun DisplayNIP05(
|
||||
nip05State: Nip05State.Exists,
|
||||
nip05VerifState: Nip05VerifState,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
if (nip05State.nip05.name != "_") {
|
||||
Text(
|
||||
text = remember(nip05State) { AnnotatedString(nip05State.nip05.name) },
|
||||
fontSize = Font14SP,
|
||||
color = MaterialTheme.colorScheme.nip05,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
RenderNIP05VerifiedSymbol(nip05VerifState, 1, NIP05IconSize, accountViewModel)
|
||||
|
||||
ClickableTextPrimary(
|
||||
text = nip05State.nip05.domain,
|
||||
onClick = { runCatching { uri.openUri("https://${nip05State.nip05.domain}") } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveAndRenderNIP05VerifiedSymbol(
|
||||
nip05State: Nip05State.Exists,
|
||||
compositionSizeReference: Int,
|
||||
modifier: Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
CrossfadeIfEnabled(targetState = nip05Verified.value, accountViewModel = accountViewModel) {
|
||||
val state by nip05State.verificationState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.isExpired()) {
|
||||
LaunchedEffect(key1 = state) {
|
||||
accountViewModel.runOnIO {
|
||||
nip05State.checkAndUpdate(Amethyst.instance.nip05Client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderNIP05VerifiedSymbol(state, compositionSizeReference, modifier, accountViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderNIP05VerifiedSymbol(
|
||||
state: Nip05VerifState,
|
||||
compositionSizeReference: Int,
|
||||
modifier: Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
CrossfadeIfEnabled(targetState = state, accountViewModel = accountViewModel) {
|
||||
when (it) {
|
||||
null ->
|
||||
is Nip05VerifState.Verifying ->
|
||||
Icon(
|
||||
imageVector = Icons.Default.Downloading,
|
||||
contentDescription = stringRes(id = R.string.nip05_checking),
|
||||
modifier = modifier,
|
||||
tint = Color.Yellow,
|
||||
)
|
||||
true ->
|
||||
is Nip05VerifState.NotStarted ->
|
||||
Icon(
|
||||
imageVector = Icons.Default.Downloading,
|
||||
contentDescription = stringRes(id = R.string.nip05_checking),
|
||||
modifier = modifier,
|
||||
tint = Color.Yellow,
|
||||
)
|
||||
is Nip05VerifState.Verified ->
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.nip_05, compositionSizeReference),
|
||||
contentDescription = stringRes(id = R.string.nip05_verified),
|
||||
modifier = modifier,
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
false ->
|
||||
is Nip05VerifState.Failed ->
|
||||
Icon(
|
||||
imageVector = Icons.Default.Report,
|
||||
contentDescription = stringRes(id = R.string.nip05_failed),
|
||||
modifier = modifier,
|
||||
tint = Color.Red,
|
||||
)
|
||||
is Nip05VerifState.Error ->
|
||||
Icon(
|
||||
imageVector = Icons.Default.Report,
|
||||
contentDescription = stringRes(id = R.string.nip05_failed),
|
||||
@@ -429,50 +446,3 @@ private fun NIP05VerifiedSymbol(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayNip05ProfileStatus(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
user.nip05()?.let { nip05 ->
|
||||
if (nip05.split("@").size <= 2) {
|
||||
val nip05Verified = nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
NIP05VerifiedSymbol(nip05Verified, 2, Size16Modifier, accountViewModel)
|
||||
var domainPadStart = 5.dp
|
||||
|
||||
val (user, domain) =
|
||||
remember(nip05) {
|
||||
val parts = nip05.split("@")
|
||||
if (parts.size == 1) {
|
||||
listOf("_", parts[0])
|
||||
} else {
|
||||
listOf(parts[0], parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
if (user != "_") {
|
||||
Text(
|
||||
text = "$user@",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
domainPadStart = 0.dp
|
||||
}
|
||||
|
||||
ClickableTextPrimary(
|
||||
text = domain,
|
||||
onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } },
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = domainPadStart),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.layouts.GenericRepostLayout
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
|
||||
@@ -71,6 +71,9 @@ import androidx.compose.ui.window.Popup
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -97,9 +100,6 @@ import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@@ -126,7 +126,7 @@ private fun ReplyInfoMention(
|
||||
val innerUserState by observeUserInfo(user, accountViewModel)
|
||||
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = "$prefix${innerUserState?.bestName()}",
|
||||
clickablePart = "$prefix${innerUserState?.info?.bestName()}",
|
||||
tags = innerUserState?.tags,
|
||||
style =
|
||||
LocalTextStyle.current.copy(
|
||||
|
||||
@@ -354,7 +354,7 @@ fun BaseUserPicture(
|
||||
outerModifier: Modifier = Modifier.size(size),
|
||||
) {
|
||||
Box(outerModifier, contentAlignment = Alignment.TopEnd) {
|
||||
LoadUserProfilePicture(baseUser, accountViewModel) { userProfilePicture, userName ->
|
||||
WatchProfilePicture(baseUser, accountViewModel) { userProfilePicture, userName ->
|
||||
InnerUserPicture(
|
||||
userHex = baseUser.pubkeyHex,
|
||||
userPicture = userProfilePicture,
|
||||
@@ -420,8 +420,8 @@ fun ObserveAndDrawInnerUserPicture(
|
||||
|
||||
InnerUserPicture(
|
||||
userHex = user.pubkeyHex,
|
||||
userPicture = userProfile?.profilePicture(),
|
||||
userName = userProfile?.bestName(),
|
||||
userPicture = userProfile?.info?.profilePicture(),
|
||||
userName = userProfile?.info?.bestName(),
|
||||
size = size,
|
||||
modifier = innerModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -582,31 +582,14 @@ fun ScoreTagSmallest(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadUserProfilePicture(
|
||||
fun WatchProfilePicture(
|
||||
baseUser: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
innerContent: @Composable (String?, String?) -> Unit,
|
||||
) {
|
||||
val userProfile by observeUserInfo(baseUser, accountViewModel)
|
||||
|
||||
innerContent(userProfile?.profilePicture(), userProfile?.bestName())
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadUserProfilePicture(
|
||||
baseUserHex: HexKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
innerContent: @Composable (String?, String?) -> Unit,
|
||||
) {
|
||||
LoadUser(baseUserHex, accountViewModel) {
|
||||
if (it != null) {
|
||||
val userProfile by observeUserInfo(it, accountViewModel)
|
||||
|
||||
innerContent(userProfile?.profilePicture(), userProfile?.bestName())
|
||||
} else {
|
||||
innerContent(null, null)
|
||||
}
|
||||
}
|
||||
innerContent(userProfile?.info?.profilePicture(), userProfile?.info?.bestName())
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
@@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdButtonSizeModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
@Composable
|
||||
@@ -105,7 +105,7 @@ fun UsernameDisplay(
|
||||
val userMetadata by observeUserInfo(baseUser, accountViewModel)
|
||||
|
||||
CrossfadeIfEnabled(targetState = userMetadata, modifier = weight, label = "UsernameDisplay", accountViewModel = accountViewModel) {
|
||||
val name = it?.bestName()
|
||||
val name = it?.info?.bestName()
|
||||
if (name != null) {
|
||||
UserDisplay(name, it.tags, weight, fontWeight, textColor)
|
||||
} else {
|
||||
|
||||
+5
-5
@@ -21,7 +21,9 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.invoice
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@@ -30,11 +32,9 @@ fun NewPostInvoiceRequest(
|
||||
onSuccess: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val lnAddress =
|
||||
accountViewModel.account
|
||||
.userProfile()
|
||||
.info
|
||||
?.lnAddress()
|
||||
val user = accountViewModel.account.userProfile()
|
||||
val info by observeUserInfo(user, accountViewModel)
|
||||
val lnAddress = info?.info?.lnAddress()
|
||||
|
||||
if (lnAddress != null) {
|
||||
InvoiceRequest(
|
||||
|
||||
+5
-5
@@ -38,7 +38,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -86,11 +86,11 @@ private fun DisplayUserNameWithDeleteMark(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val innerUserState by observeUser(user, accountViewModel)
|
||||
innerUserState?.user?.let { myUser ->
|
||||
val innerUserState by observeUserInfo(user, accountViewModel)
|
||||
innerUserState?.let { meta ->
|
||||
CreateTextWithEmoji(
|
||||
text = remember(innerUserState) { "✖ ${myUser.toBestDisplayName()}" },
|
||||
tags = myUser.info?.tags,
|
||||
text = remember(meta) { "✖ ${meta.info.bestName() ?: user.pubkeyDisplayHex()}" },
|
||||
tags = meta.tags,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
+1
-1
@@ -26,6 +26,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
|
||||
import com.vitorpamplona.amethyst.service.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableTextColor
|
||||
@@ -35,7 +36,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
@@ -79,18 +79,17 @@ fun ForkInformationRowLightColor(
|
||||
|
||||
if (route != null) {
|
||||
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
val userState by observeUser(author, accountViewModel)
|
||||
userState?.user?.toBestDisplayName()?.let {
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = stringRes(id = R.string.forked_from) + " " + it,
|
||||
maxLines = 1,
|
||||
route = route,
|
||||
overrideColor = MaterialTheme.colorScheme.primary,
|
||||
fontSize = Font14SP,
|
||||
nav = nav,
|
||||
tags = userState?.user?.info?.tags,
|
||||
)
|
||||
}
|
||||
val userState by observeUserInfo(author, accountViewModel)
|
||||
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = stringRes(id = R.string.forked_from) + " " + (userState?.info?.bestName() ?: author.pubkeyDisplayHex()),
|
||||
maxLines = 1,
|
||||
route = route,
|
||||
overrideColor = MaterialTheme.colorScheme.primary,
|
||||
fontSize = Font14SP,
|
||||
nav = nav,
|
||||
tags = userState?.tags,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -176,11 +176,11 @@ open class CommentPostViewModel :
|
||||
var wantsZapraiser by mutableStateOf(false)
|
||||
override val zapRaiserAmount = mutableStateOf<Long?>(null)
|
||||
|
||||
fun lnAddress(): String? = account.userProfile().info?.lnAddress()
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account.userProfile().info?.lnAddress() != null
|
||||
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
||||
|
||||
fun user(): User? = account.userProfile()
|
||||
fun user(): User = account.userProfile()
|
||||
|
||||
open fun init(accountVM: AccountViewModel) {
|
||||
this.accountViewModel = accountVM
|
||||
@@ -255,8 +255,8 @@ open class CommentPostViewModel :
|
||||
private fun loadFromDraft(draftEvent: CommentEvent) {
|
||||
this.externalIdentity = draftEvent.scope()
|
||||
|
||||
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canAddInvoice = accountViewModel.userProfile().lnAddress() != null
|
||||
canAddZapRaiser = accountViewModel.userProfile().lnAddress() != null
|
||||
multiOrchestrator = null
|
||||
|
||||
val localForwardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" }
|
||||
|
||||
@@ -55,6 +55,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary
|
||||
@@ -69,8 +71,6 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView
|
||||
@@ -56,7 +57,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.experimental.audio.track.tags.ParticipantTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -95,8 +97,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag
|
||||
|
||||
@@ -41,6 +41,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap
|
||||
@@ -61,8 +63,6 @@ import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.blackTagModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@@ -40,6 +40,8 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
@@ -61,8 +63,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
@@ -56,7 +57,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
@@ -306,7 +306,7 @@ fun DisplayEntryForUser(
|
||||
val userMetadata by observeUserInfo(baseUser, accountViewModel)
|
||||
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = userMetadata?.bestName() ?: baseUser.pubkeyDisplayHex(),
|
||||
clickablePart = userMetadata?.info?.bestName() ?: baseUser.pubkeyDisplayHex(),
|
||||
maxLines = 1,
|
||||
route = remember(baseUser) { routeFor(baseUser) },
|
||||
nav = nav,
|
||||
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
@@ -48,7 +49,6 @@ import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@Composable
|
||||
|
||||
+2
-2
@@ -26,14 +26,14 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
|
||||
@@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ 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.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
@@ -53,7 +54,6 @@ import com.vitorpamplona.amethyst.ui.note.PinIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.getGradient
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
|
||||
@@ -32,6 +32,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
@@ -44,8 +46,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
|
||||
@@ -35,6 +35,8 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
@@ -48,8 +50,6 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.RenderUserAsClickableText
|
||||
@@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfHalfTopPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
@@ -34,7 +35,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
@@ -45,8 +47,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
|
||||
@@ -44,6 +44,7 @@ import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.ui.actions.EditPostView
|
||||
@@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -42,6 +42,8 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
@@ -59,9 +61,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip71Video.VideoEvent
|
||||
|
||||
+17
-11
@@ -33,7 +33,6 @@ import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
|
||||
import com.vitorpamplona.amethyst.model.DefaultNIP65List
|
||||
import com.vitorpamplona.amethyst.model.DefaultNIP65RelaySet
|
||||
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
||||
import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
@@ -42,6 +41,7 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
|
||||
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
@@ -214,16 +214,22 @@ class AccountStateViewModel : ViewModel() {
|
||||
loginSync(newKey, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
}
|
||||
} else if (EMAIL_PATTERN.matcher(key).matches()) {
|
||||
Nip05NostrAddressVerifier.verifyNip05(
|
||||
key,
|
||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(false) },
|
||||
onSuccess = { publicKey ->
|
||||
loginSync(Hex.decode(publicKey).toNpub(), transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
},
|
||||
onError = {
|
||||
onError(it)
|
||||
},
|
||||
)
|
||||
val nip05 = Nip05Id.parse(key)
|
||||
if (nip05 == null) {
|
||||
onError("Could not parse nip05 address: $nip05")
|
||||
} else {
|
||||
try {
|
||||
val pubkeyInfo = Amethyst.instance.nip05Client.get(nip05)
|
||||
if (pubkeyInfo == null) {
|
||||
onError("User not found in the nip05 server: $nip05")
|
||||
} else {
|
||||
loginSync(Hex.decode(pubkeyInfo.pubkey).toNpub(), transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError("Could not load nip05 address from the server: $nip05. ${e.message}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loginSync(key, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
}
|
||||
|
||||
+9
-42
@@ -57,7 +57,6 @@ import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator
|
||||
import com.vitorpamplona.amethyst.model.privacyOptions.EmptyRoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.model.privacyOptions.IRoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||
@@ -91,7 +90,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.EmptyIAuthStatus
|
||||
@@ -929,45 +927,7 @@ class AccountViewModel(
|
||||
return note.getReactionBy(userProfile())
|
||||
}
|
||||
|
||||
fun verifyNip05(
|
||||
userMetadata: UserMetadata,
|
||||
pubkeyHex: String,
|
||||
onResult: (Boolean) -> Unit,
|
||||
) {
|
||||
val nip05 = userMetadata.nip05?.ifBlank { null } ?: return
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Nip05NostrAddressVerifier
|
||||
.verifyNip05(
|
||||
nip05,
|
||||
okHttpClient = httpClientBuilder::okHttpClientForNip05,
|
||||
onSuccess = {
|
||||
// Marks user as verified
|
||||
if (it == pubkeyHex) {
|
||||
userMetadata.nip05Verified = true
|
||||
userMetadata.nip05LastVerificationTime = TimeUtils.now()
|
||||
|
||||
onResult(userMetadata.nip05Verified)
|
||||
} else {
|
||||
userMetadata.nip05Verified = false
|
||||
userMetadata.nip05LastVerificationTime = 0
|
||||
|
||||
onResult(userMetadata.nip05Verified)
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
userMetadata.nip05LastVerificationTime = 0
|
||||
userMetadata.nip05Verified = false
|
||||
|
||||
Log.d("NIP05 Error", it)
|
||||
|
||||
onResult(userMetadata.nip05Verified)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun runOnIO(runOnIO: () -> Unit) {
|
||||
fun runOnIO(runOnIO: suspend () -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) { runOnIO() }
|
||||
}
|
||||
|
||||
@@ -1243,7 +1203,14 @@ class AccountViewModel(
|
||||
context: Context,
|
||||
onDone: (String, String) -> Unit,
|
||||
) {
|
||||
val lud16 = account.userProfile().info?.lud16
|
||||
val lud16 =
|
||||
account
|
||||
.userProfile()
|
||||
.metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.lud16
|
||||
if (lud16 != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
||||
+4
-4
@@ -25,6 +25,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
@@ -38,7 +39,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.UserDisplayNameLayout
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5Modifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
|
||||
@Composable
|
||||
fun DrawAuthorInfo(
|
||||
@@ -63,8 +63,8 @@ private fun WatchAndDisplayUser(
|
||||
picture = {
|
||||
InnerUserPicture(
|
||||
userHex = author.pubkeyHex,
|
||||
userPicture = userState?.picture,
|
||||
userName = userState?.bestName(),
|
||||
userPicture = userState?.info?.picture,
|
||||
userName = userState?.info?.bestName(),
|
||||
size = Size20dp,
|
||||
modifier = Modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -81,7 +81,7 @@ private fun WatchAndDisplayUser(
|
||||
name = {
|
||||
if (userState != null) {
|
||||
CreateTextWithEmoji(
|
||||
text = userState?.bestName() ?: author.pubkeyDisplayHex(),
|
||||
text = userState?.info?.bestName() ?: author.pubkeyDisplayHex(),
|
||||
tags = userState?.tags ?: EmptyTagList,
|
||||
maxLines = 1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
|
||||
+1
-1
@@ -24,10 +24,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
|
||||
@Composable
|
||||
|
||||
+3
-3
@@ -48,6 +48,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Constants
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
@@ -66,10 +69,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
|
||||
+1
-1
@@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
|
||||
import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo
|
||||
@@ -40,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.utils.ciphers.AESGCM
|
||||
|
||||
+2
-2
@@ -26,6 +26,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
@@ -33,8 +35,6 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
|
||||
@Composable
|
||||
fun RenderRegularTextNote(
|
||||
|
||||
+6
-2
@@ -50,14 +50,18 @@ fun filterNip04DMs(
|
||||
val outbox =
|
||||
authorHomeRelayEvent?.writeRelaysNorm()
|
||||
?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null }
|
||||
?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay)
|
||||
?: listOfNotNull(
|
||||
LocalCache.getUserIfExists(it)?.metadataOrNull()?.relay,
|
||||
)
|
||||
|
||||
groupOutboxRelays.addAll(outbox)
|
||||
|
||||
val inbox =
|
||||
authorHomeRelayEvent?.readRelaysNorm()?.ifEmpty { null }
|
||||
?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null }
|
||||
?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay)
|
||||
?: listOfNotNull(
|
||||
LocalCache.getUserIfExists(it)?.metadataOrNull()?.relay,
|
||||
)
|
||||
|
||||
groupInboxRelays.addAll(inbox)
|
||||
}
|
||||
|
||||
+9
-4
@@ -30,7 +30,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserShortName
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
@@ -165,12 +165,17 @@ fun ShortUsernameDisplay(
|
||||
fontWeight: FontWeight = FontWeight.Bold,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val userName by observeUserShortName(baseUser, accountViewModel)
|
||||
val userInfo by observeUserInfo(baseUser, accountViewModel)
|
||||
|
||||
CrossfadeIfEnabled(targetState = userName, modifier = weight, accountViewModel = accountViewModel) {
|
||||
val firstName =
|
||||
remember(userInfo) {
|
||||
userInfo?.info?.firstName() ?: baseUser.pubkeyDisplayHex()
|
||||
}
|
||||
|
||||
CrossfadeIfEnabled(targetState = firstName, modifier = weight, accountViewModel = accountViewModel) {
|
||||
CreateTextWithEmoji(
|
||||
text = it,
|
||||
tags = baseUser.info?.tags,
|
||||
tags = userInfo?.tags,
|
||||
fontWeight = fontWeight,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
+3
-3
@@ -175,11 +175,11 @@ class ChatNewMessageViewModel :
|
||||
// NIP17 Wrapped DMs / Group messages
|
||||
var nip17 by mutableStateOf(false)
|
||||
|
||||
fun lnAddress(): String? = account.userProfile().info?.lnAddress()
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account.userProfile().info?.lnAddress() != null
|
||||
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
||||
|
||||
fun user(): User? = account.userProfile()
|
||||
fun user(): User = account.userProfile()
|
||||
|
||||
fun init(accountVM: AccountViewModel) {
|
||||
this.accountViewModel = accountVM
|
||||
|
||||
+1
-1
@@ -41,6 +41,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
@@ -60,7 +61,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutabl
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||
|
||||
+3
-3
@@ -155,11 +155,11 @@ open class ChannelNewMessageViewModel :
|
||||
var wantsZapraiser by mutableStateOf(false)
|
||||
var zapRaiserAmount by mutableStateOf<Long?>(null)
|
||||
|
||||
fun lnAddress(): String? = account.userProfile().info?.lnAddress()
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account.userProfile().info?.lnAddress() != null
|
||||
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
||||
|
||||
fun user(): User? = account.userProfile()
|
||||
fun user(): User = account.userProfile()
|
||||
|
||||
open fun init(accountVM: AccountViewModel) {
|
||||
this.accountViewModel = accountVM
|
||||
|
||||
+3
-3
@@ -169,11 +169,11 @@ open class NewProductViewModel :
|
||||
|
||||
var relayList by mutableStateOf<ImmutableList<NormalizedRelayUrl>?>(null)
|
||||
|
||||
fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress()
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null
|
||||
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
||||
|
||||
fun user(): User? = account?.userProfile()
|
||||
fun user(): User = account.userProfile()
|
||||
|
||||
open fun init(accountVM: AccountViewModel) {
|
||||
this.accountViewModel = accountVM
|
||||
|
||||
+6
-6
@@ -260,9 +260,9 @@ open class ShortNotePostViewModel :
|
||||
var wantsZapRaiser by mutableStateOf(false)
|
||||
override val zapRaiserAmount = mutableStateOf<Long?>(null)
|
||||
|
||||
fun lnAddress(): String? = account.userProfile().info?.lnAddress()
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account.userProfile().info?.lnAddress() != null
|
||||
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
||||
|
||||
fun user(): User = account.userProfile()
|
||||
|
||||
@@ -331,8 +331,8 @@ open class ShortNotePostViewModel :
|
||||
|
||||
val user = account.userProfile()
|
||||
|
||||
canAddInvoice = user.info?.lnAddress() != null
|
||||
canAddZapRaiser = user.info?.lnAddress() != null
|
||||
canAddInvoice = user.lnAddress() != null
|
||||
canAddZapRaiser = user.lnAddress() != null
|
||||
canUsePoll = originalNote == null
|
||||
multiOrchestrator = null
|
||||
|
||||
@@ -417,8 +417,8 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draftEvent: TextNoteEvent) {
|
||||
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canAddInvoice = accountViewModel.userProfile().lnAddress() != null
|
||||
canAddZapRaiser = accountViewModel.userProfile().lnAddress() != null
|
||||
multiOrchestrator = null
|
||||
|
||||
val localForwardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" }
|
||||
|
||||
+1
-1
@@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadedFeedState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
|
||||
+3
-3
@@ -178,11 +178,11 @@ class NewPublicMessageViewModel :
|
||||
var wantsZapraiser by mutableStateOf(false)
|
||||
override var zapRaiserAmount = mutableStateOf<Long?>(null)
|
||||
|
||||
fun lnAddress(): String? = account.userProfile().info?.lnAddress()
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account.userProfile().info?.lnAddress() != null
|
||||
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
||||
|
||||
fun user(): User? = account.userProfile()
|
||||
fun user(): User = account.userProfile()
|
||||
|
||||
fun init(accountVM: AccountViewModel) {
|
||||
this.accountViewModel = accountVM
|
||||
|
||||
+70
-38
@@ -42,18 +42,21 @@ import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.DrawPlayName
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations
|
||||
@@ -61,16 +64,16 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserApp
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges.DisplayBadges
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.TelegramIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims
|
||||
|
||||
private const val IDENTITY_ICON_CACHE_KEY = 0
|
||||
|
||||
@@ -81,34 +84,34 @@ fun DrawAdditionalInfo(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val userState by observeUser(baseUser, accountViewModel)
|
||||
val user = userState?.user ?: return
|
||||
val userState by observeUserInfo(baseUser, accountViewModel)
|
||||
val user = userState ?: return
|
||||
val uri = LocalUriHandler.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
user.toBestDisplayName().let {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 7.dp)) {
|
||||
CreateTextWithEmoji(
|
||||
text = it,
|
||||
tags = user.info?.tags ?: EmptyTagList,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 25.sp,
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 7.dp)) {
|
||||
CreateTextWithEmoji(
|
||||
text = user.info.bestName() ?: baseUser.pubkeyDisplayHex(),
|
||||
tags = user.tags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 25.sp,
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
user.info.pronouns?.let {
|
||||
Text(
|
||||
text = "($it)",
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
user.info?.pronouns?.let {
|
||||
Text(
|
||||
text = "($it)",
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
user.info.bestName()?.let {
|
||||
DrawPlayName(it)
|
||||
}
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = user.pubkeyDisplayHex(),
|
||||
text = baseUser.pubkeyDisplayHex(),
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
@@ -118,7 +121,7 @@ fun DrawAdditionalInfo(
|
||||
Modifier
|
||||
.size(25.dp)
|
||||
.padding(start = 5.dp),
|
||||
onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())) },
|
||||
onClick = { clipboardManager.setText(AnnotatedString(baseUser.pubkeyNpub())) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
@@ -130,7 +133,7 @@ fun DrawAdditionalInfo(
|
||||
|
||||
IconButton(
|
||||
modifier = Size25Modifier,
|
||||
onClick = { nav.nav(Route.QRDisplay(user.pubkeyHex)) },
|
||||
onClick = { nav.nav(Route.QRDisplay(baseUser.pubkeyHex)) },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.ic_qrcode, 1),
|
||||
@@ -143,9 +146,9 @@ fun DrawAdditionalInfo(
|
||||
|
||||
DisplayBadges(baseUser, accountViewModel, nav)
|
||||
|
||||
DisplayNip05ProfileStatus(user, accountViewModel)
|
||||
DisplayNip05ProfileStatus(baseUser, accountViewModel)
|
||||
|
||||
val website = user.info?.website
|
||||
val website = user.info.website
|
||||
if (!website.isNullOrEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
@@ -175,21 +178,13 @@ fun DrawAdditionalInfo(
|
||||
|
||||
val lud16 =
|
||||
remember(userState) {
|
||||
userState
|
||||
?.user
|
||||
?.info
|
||||
?.lud16
|
||||
?.trim() ?: userState
|
||||
?.user
|
||||
?.info
|
||||
?.lud06
|
||||
?.trim()
|
||||
userState?.info?.lud16?.trim()
|
||||
?: userState?.info?.lud06?.trim()
|
||||
}
|
||||
DisplayLNAddress(lud16, baseUser, accountViewModel, nav)
|
||||
|
||||
val identities = user.latestMetadata?.identityClaims()
|
||||
if (!identities.isNullOrEmpty()) {
|
||||
identities.forEach { identity: IdentityClaimTag ->
|
||||
if (user.identities.isNotEmpty()) {
|
||||
user.identities.forEach { identity: IdentityClaimTag ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
tint = Color.Unspecified,
|
||||
@@ -210,7 +205,7 @@ fun DrawAdditionalInfo(
|
||||
}
|
||||
}
|
||||
|
||||
user.info?.about?.let {
|
||||
user.info.about?.let {
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp),
|
||||
) {
|
||||
@@ -221,7 +216,7 @@ fun DrawAdditionalInfo(
|
||||
content = it,
|
||||
canPreview = false,
|
||||
quotesLeft = 1,
|
||||
tags = EmptyTagList,
|
||||
tags = user.tags,
|
||||
backgroundColor = background,
|
||||
id = it,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -233,6 +228,43 @@ fun DrawAdditionalInfo(
|
||||
DisplayAppRecommendations(appRecommendations, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayNip05ProfileStatus(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val nip05StateMetadata by user.nip05State().flow.collectAsStateWithLifecycle()
|
||||
|
||||
when (val nip05State = nip05StateMetadata) {
|
||||
is Nip05State.Exists -> {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = SpacedBy5dp) {
|
||||
ObserveAndRenderNIP05VerifiedSymbol(nip05State, 2, Size16Modifier, accountViewModel)
|
||||
|
||||
if (nip05State.nip05.name != "_") {
|
||||
Text(
|
||||
text = "$user@",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
ClickableTextPrimary(
|
||||
text = nip05State.nip05.domain,
|
||||
onClick = { runCatching { uri.openUri("https://${nip05State.nip05.domain}") } },
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
|
||||
fun getIdentityClaimIcon(identity: IdentityClaimTag): Int =
|
||||
when (identity) {
|
||||
is TwitterIdentity -> R.drawable.x
|
||||
|
||||
+38
-23
@@ -47,6 +47,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
@@ -73,7 +74,6 @@ fun ProfileHeader(
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
var zoomImageDialogOpen by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
DrawBanner(baseUser, accountViewModel)
|
||||
@@ -126,27 +126,10 @@ fun ProfileHeader(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
val clipboardManager = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
ClickableUserPicture(
|
||||
ZoomableUserPicture(
|
||||
baseUser = baseUser,
|
||||
accountViewModel = accountViewModel,
|
||||
size = Size100dp,
|
||||
modifier = MaterialTheme.colorScheme.userProfileBorderModifier,
|
||||
onClick = {
|
||||
if (baseUser.profilePicture() != null) {
|
||||
zoomImageDialogOpen = true
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
it.info?.picture?.let { it1 ->
|
||||
scope.launch {
|
||||
val clipData = ClipData.newPlainText("profile picture url", it1)
|
||||
clipboardManager.setClipEntry(ClipEntry(clipData))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Row(
|
||||
@@ -164,12 +147,44 @@ fun ProfileHeader(
|
||||
HorizontalDivider(modifier = Modifier.padding(top = 6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val profilePic = baseUser.profilePicture()
|
||||
if (zoomImageDialogOpen && profilePic != null) {
|
||||
@Composable
|
||||
fun ZoomableUserPicture(
|
||||
baseUser: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
size: Dp,
|
||||
) {
|
||||
val clipboardManager = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var zoomImageUrl by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
ClickableUserPicture(
|
||||
baseUser = baseUser,
|
||||
accountViewModel = accountViewModel,
|
||||
size = size,
|
||||
modifier = MaterialTheme.colorScheme.userProfileBorderModifier,
|
||||
onClick = {
|
||||
val pic = baseUser.profilePicture()
|
||||
if (pic != null) {
|
||||
zoomImageUrl = pic
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
baseUser.profilePicture()?.let { it1 ->
|
||||
scope.launch {
|
||||
val clipData = ClipData.newPlainText("profile picture url", it1)
|
||||
clipboardManager.setClipEntry(ClipEntry(clipData))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
zoomImageUrl?.let {
|
||||
ZoomableImageDialog(
|
||||
RichTextParser.parseImageOrVideo(profilePic),
|
||||
onDismiss = { zoomImageDialogOpen = false },
|
||||
RichTextParser.parseImageOrVideo(it),
|
||||
onDismiss = { zoomImageUrl = null },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
+88
-58
@@ -48,16 +48,18 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayNIP05
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveAndDisplayNIP05
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
@@ -67,21 +69,34 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ShowQRScreenPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
accountViewModel.userProfile().info =
|
||||
UserMetadata().apply {
|
||||
name = "My Name"
|
||||
picture = "Picture"
|
||||
nip05 = null
|
||||
banner = "http://banner.com/test"
|
||||
website = "http://mywebsite.com/test"
|
||||
about = "This is the about me"
|
||||
}
|
||||
runBlocking {
|
||||
accountViewModel.userProfile().metadata().newMetadata(
|
||||
UserMetadata().apply {
|
||||
name = "My Name"
|
||||
picture = "Picture"
|
||||
nip05 = null
|
||||
banner = "http://banner.com/test"
|
||||
website = "http://mywebsite.com/test"
|
||||
about = "This is the about me"
|
||||
},
|
||||
MetadataEvent(
|
||||
id = "",
|
||||
pubKey = "",
|
||||
createdAt = 0,
|
||||
tags = emptyArray(),
|
||||
content = "",
|
||||
sig = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
ShowQRScreen(
|
||||
pubkey = accountViewModel.userProfile().pubkeyHex,
|
||||
@@ -178,53 +193,7 @@ fun PresentQR(
|
||||
accountViewModel: AccountViewModel,
|
||||
switchToScan: () -> Unit,
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = user.pubkeyHex,
|
||||
model = user.profilePicture(),
|
||||
contentDescription = stringRes(R.string.profile_image),
|
||||
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
|
||||
) {
|
||||
CreateTextWithEmoji(
|
||||
text = user.info?.bestName() ?: "",
|
||||
tags = user.info?.tags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
) {
|
||||
val nip05 = user.nip05()
|
||||
if (nip05 != null) {
|
||||
val nip05Verified =
|
||||
nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel)
|
||||
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
} else {
|
||||
Text(
|
||||
text = user.pubkeyDisplayHex(),
|
||||
fontSize = Font14SP,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
RenderName(user, accountViewModel)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
@@ -243,3 +212,64 @@ fun PresentQR(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderName(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
Column {
|
||||
val userInfo by observeUserInfo(user, accountViewModel)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = user.pubkeyHex,
|
||||
model = userInfo?.info?.profilePicture(),
|
||||
contentDescription = stringRes(R.string.profile_image),
|
||||
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
|
||||
) {
|
||||
CreateTextWithEmoji(
|
||||
text = userInfo?.info?.bestName() ?: "",
|
||||
tags = userInfo?.tags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
) {
|
||||
WatchAndDisplayNip05Row(user, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchAndDisplayNip05Row(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val nip05StateMetadata by user.nip05State().flow.collectAsStateWithLifecycle()
|
||||
|
||||
when (val nip05State = nip05StateMetadata) {
|
||||
is Nip05State.Exists -> ObserveAndDisplayNIP05(nip05State, accountViewModel)
|
||||
else ->
|
||||
Text(
|
||||
text = user.pubkeyDisplayHex(),
|
||||
fontSize = Font14SP,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@@ -80,7 +81,6 @@ import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
@@ -96,6 +96,7 @@ import com.vitorpamplona.amethyst.ui.note.LongPressToQuickAction
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderApproveButton
|
||||
@@ -784,6 +785,7 @@ private fun RenderApprovalIfNeeded(
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Composable
|
||||
private fun RenderClassifiedsReaderForThread(
|
||||
noteEvent: ClassifiedsEvent,
|
||||
@@ -923,7 +925,13 @@ private fun RenderClassifiedsReaderForThread(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val sellerName = note.author?.info?.bestName()
|
||||
val sellerName =
|
||||
note.author
|
||||
?.metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.bestName()
|
||||
|
||||
val msg =
|
||||
if (sellerName != null) {
|
||||
|
||||
+1
-1
@@ -56,7 +56,6 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
@@ -75,6 +74,7 @@ import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderAllRelayList
|
||||
import com.vitorpamplona.amethyst.ui.note.ReplyReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
|
||||
+1
-1
@@ -51,6 +51,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
import com.vitorpamplona.amethyst.service.lang.TranslationsCache
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
@@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
+2
-2
@@ -25,12 +25,12 @@ import androidx.benchmark.junit4.measureRepeated
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.linkedin.urls.detection.UrlDetector
|
||||
import com.linkedin.urls.detection.UrlDetectorOptions
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.ImageSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.LinkSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import junit.framework.TestCase.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.commons.richtext
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.commons.richtext
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.core
|
||||
package com.vitorpamplona.amethyst.commons.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@@ -33,7 +33,6 @@ import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
@@ -231,7 +230,7 @@ open class Note(
|
||||
|
||||
return relays.firstOrNull()
|
||||
} else {
|
||||
currentOutbox?.firstOrNull() ?: author?.latestMetadataRelay
|
||||
currentOutbox?.firstOrNull() ?: author?.metadataOrNull()?.relay
|
||||
}
|
||||
}
|
||||
|
||||
@@ -973,9 +972,8 @@ class NoteFlowSet(
|
||||
fun author() =
|
||||
metadata.stateFlow.flatMapLatest {
|
||||
it.note.author
|
||||
?.flow()
|
||||
?.metadata
|
||||
?.stateFlow ?: MutableStateFlow(null)
|
||||
?.metadata()
|
||||
?.flow ?: MutableStateFlow(null)
|
||||
}
|
||||
|
||||
fun isInUse(): Boolean =
|
||||
|
||||
@@ -22,12 +22,13 @@ package com.vitorpamplona.amethyst.commons.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.nip01Core.UserMetadataCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.UserNip05Cache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip56Reports.UserReportCache
|
||||
import com.vitorpamplona.amethyst.commons.model.trustedAssertions.UserCardsCache
|
||||
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
||||
import com.vitorpamplona.quartz.lightning.Lud06
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -52,15 +53,11 @@ class User(
|
||||
val nip65RelayListNote: Note,
|
||||
val dmRelayListNote: Note,
|
||||
) {
|
||||
private var metadata: UserMetadataCache? = null
|
||||
private var reports: UserReportCache? = null
|
||||
private var cards: UserCardsCache? = null
|
||||
private var nip05: UserNip05Cache? = null
|
||||
|
||||
// private var deps = ScatterMap<KClass<out UserDependencies>, UserDependencies>()
|
||||
|
||||
var info: UserMetadata? = null
|
||||
|
||||
var latestMetadata: MetadataEvent? = null
|
||||
var latestMetadataRelay: NormalizedRelayUrl? = null
|
||||
var latestContactList: ContactListEvent? = null
|
||||
|
||||
var zaps = mapOf<Note, Note?>()
|
||||
@@ -85,39 +82,45 @@ class User(
|
||||
|
||||
fun outboxRelays() = authorRelayList()?.writeRelaysNorm()
|
||||
|
||||
fun relayHints() = authorRelayList()?.writeRelaysNorm()?.take(3) ?: listOfNotNull(latestMetadataRelay)
|
||||
fun relayHints() = authorRelayList()?.writeRelaysNorm()?.take(3) ?: listOfNotNull(metadataOrNull()?.relay)
|
||||
|
||||
fun inboxRelays() = authorRelayList()?.readRelaysNorm()
|
||||
|
||||
fun dmInboxRelays() = dmInboxRelayList()?.relays()?.ifEmpty { null } ?: inboxRelays()
|
||||
|
||||
fun bestRelayHint() = authorRelayList()?.writeRelaysNorm()?.firstOrNull() ?: latestMetadataRelay
|
||||
fun bestRelayHint() = authorRelayList()?.writeRelaysNorm()?.firstOrNull() ?: metadataOrNull()?.relay
|
||||
|
||||
fun toPTag() = PTag(pubkeyHex, bestRelayHint())
|
||||
|
||||
fun toNostrUri() = "nostr:${toNProfile()}"
|
||||
|
||||
fun toBestShortFirstName(): String {
|
||||
val fullName = toBestDisplayName()
|
||||
fun toBestDisplayName(): String =
|
||||
metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.bestName() ?: pubkeyDisplayHex()
|
||||
|
||||
val names = fullName.split(' ')
|
||||
fun nip05(): String? =
|
||||
metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.nip05
|
||||
|
||||
val firstName =
|
||||
if (names[0].length <= 3) {
|
||||
// too short. Remove Dr.
|
||||
"${names[0]} ${names.getOrNull(1) ?: ""}"
|
||||
} else {
|
||||
names[0]
|
||||
}
|
||||
fun profilePicture(): String? =
|
||||
metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.picture
|
||||
|
||||
return firstName
|
||||
}
|
||||
|
||||
fun toBestDisplayName(): String = info?.bestName() ?: pubkeyDisplayHex()
|
||||
|
||||
fun nip05(): String? = info?.nip05
|
||||
|
||||
fun profilePicture(): String? = info?.picture
|
||||
fun lnAddress(): String? =
|
||||
metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.lnAddress()
|
||||
|
||||
fun updateContactList(event: ContactListEvent): Set<HexKey> {
|
||||
if (event.id == latestContactList?.id) return emptySet()
|
||||
@@ -184,21 +187,30 @@ class User(
|
||||
|
||||
fun updateUserInfo(
|
||||
newUserInfo: UserMetadata,
|
||||
latestMetadata: MetadataEvent,
|
||||
metaEvent: MetadataEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
) {
|
||||
info = newUserInfo
|
||||
info?.tags = latestMetadata.tags.toImmutableListOfLists()
|
||||
info?.cleanBlankNames()
|
||||
newUserInfo.cleanBlankNames()
|
||||
|
||||
// converts lud06 to lud16
|
||||
if (newUserInfo.lud16.isNullOrBlank()) {
|
||||
info?.lud06?.let {
|
||||
newUserInfo.lud06?.let {
|
||||
if (it.lowercase().startsWith("lnurl")) {
|
||||
info?.lud16 = Lud06().toLud16(it)
|
||||
newUserInfo.lud16 = Lud06().toLud16(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flowSet?.metadata?.invalidateData()
|
||||
val metadata = metadata()
|
||||
|
||||
metadata.newMetadata(newUserInfo, metaEvent)
|
||||
|
||||
if (relay != null) {
|
||||
metadata.relay = relay
|
||||
}
|
||||
|
||||
// doesn't create Nip05 unless needed.
|
||||
nip05StateOrNull()?.newMetadata(newUserInfo.nip05, metaEvent.pubKey)
|
||||
}
|
||||
|
||||
fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
|
||||
@@ -209,49 +221,73 @@ class User(
|
||||
|
||||
fun reports(): UserReportCache = reports ?: UserReportCache().also { reports = it }
|
||||
|
||||
// fun reportsOrNull(): UserReports? = deps[UserReports::class] as? UserReports
|
||||
|
||||
// fun reports(): UserReports = deps.getOrPut(UserReports::class) { UserReports() } as UserReports
|
||||
|
||||
fun cardsOrNull(): UserCardsCache? = cards
|
||||
|
||||
fun cards(): UserCardsCache = cards ?: UserCardsCache().also { cards = it }
|
||||
|
||||
fun metadataOrNull(): UserMetadataCache? = metadata
|
||||
|
||||
fun metadata(): UserMetadataCache = metadata ?: UserMetadataCache().also { metadata = it }
|
||||
|
||||
fun nip05StateOrNull(): UserNip05Cache? = nip05
|
||||
|
||||
fun nip05State(): UserNip05Cache =
|
||||
nip05 ?: UserNip05Cache().also {
|
||||
nip05 = it
|
||||
val meta = metadata().flow.value
|
||||
if (meta != null) {
|
||||
it.newMetadata(meta.info.nip05, pubkeyHex)
|
||||
}
|
||||
}
|
||||
|
||||
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {
|
||||
if (hiddenWordsCase.isEmpty()) return false
|
||||
|
||||
if (toBestDisplayName().containsAny(hiddenWordsCase)) {
|
||||
return true
|
||||
}
|
||||
metadata?.flow?.value?.let { userInfo ->
|
||||
val info = userInfo.info
|
||||
|
||||
if (profilePicture()?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
if (info.name?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (info?.banner?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
if (info.displayName?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (info?.about?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
if (info.picture?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (info?.lud06?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
if (info.banner?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (info?.lud16?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
if (info.about?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (info?.nip05?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
if (info.lud06?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (info.lud16?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (info.nip05?.containsAny(hiddenWordsCase) == true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
|
||||
fun anyNameStartsWith(username: String): Boolean =
|
||||
metadata
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.anyNameStartsWith(username) ?: false
|
||||
|
||||
@Synchronized
|
||||
fun createOrDestroyFlowSync(create: Boolean) {
|
||||
@@ -285,7 +321,6 @@ class UserFlowSet(
|
||||
u: User,
|
||||
) {
|
||||
// Observers line up here.
|
||||
val metadata = UserBundledRefresherFlow(u)
|
||||
val follows = UserBundledRefresherFlow(u)
|
||||
val followers = UserBundledRefresherFlow(u)
|
||||
val usedRelays = UserBundledRefresherFlow(u)
|
||||
@@ -293,8 +328,7 @@ class UserFlowSet(
|
||||
val statuses = UserBundledRefresherFlow(u)
|
||||
|
||||
fun isInUse(): Boolean =
|
||||
metadata.hasObservers() ||
|
||||
follows.hasObservers() ||
|
||||
follows.hasObservers() ||
|
||||
followers.hasObservers() ||
|
||||
usedRelays.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.nip01Core
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
@Stable
|
||||
class UserInfo(
|
||||
val info: UserMetadata,
|
||||
val tags: ImmutableListOfLists<String>,
|
||||
val identities: List<IdentityClaimTag>,
|
||||
val createdAt: Long,
|
||||
)
|
||||
|
||||
@Stable
|
||||
class UserMetadataCache {
|
||||
val flow: MutableStateFlow<UserInfo?> = MutableStateFlow(null)
|
||||
var relay: NormalizedRelayUrl? = null
|
||||
|
||||
fun newMetadata(
|
||||
userInfo: UserMetadata,
|
||||
metaEvent: MetadataEvent,
|
||||
) {
|
||||
flow.update {
|
||||
UserInfo(
|
||||
info = userInfo,
|
||||
tags = metaEvent.tags.toImmutableListOfLists(),
|
||||
identities = metaEvent.identityClaims(),
|
||||
createdAt = metaEvent.createdAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldUpdateWith(event: MetadataEvent) = event.createdAt > (flow.value?.createdAt ?: 0)
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Stable
|
||||
sealed interface Nip05State {
|
||||
object NotFound : Nip05State
|
||||
|
||||
@Stable
|
||||
class Exists(
|
||||
val nip05: Nip05Id,
|
||||
val hexKey: HexKey,
|
||||
) : Nip05State {
|
||||
val verificationState = MutableStateFlow<Nip05VerifState>(Nip05VerifState.NotStarted)
|
||||
|
||||
fun markAsVerified() = verificationState.tryEmit(Nip05VerifState.Verified(TimeUtils.oneHourAhead()))
|
||||
|
||||
fun markAsInvalid() = verificationState.tryEmit(Nip05VerifState.Failed(TimeUtils.oneHourAhead()))
|
||||
|
||||
fun markAsVerifying() = verificationState.tryEmit(Nip05VerifState.Verifying(TimeUtils.fiveMinutesAhead()))
|
||||
|
||||
fun markAsError() = verificationState.tryEmit(Nip05VerifState.Verified(TimeUtils.fiveMinutesAhead()))
|
||||
|
||||
fun reset() = verificationState.tryEmit(Nip05VerifState.NotStarted)
|
||||
|
||||
suspend fun checkAndUpdate(nip05Client: Nip05Client) {
|
||||
println("AABBCC checkAndUpdate $nip05")
|
||||
if (verificationState.value.isExpired()) {
|
||||
println("AABBCC Veryfing $nip05")
|
||||
markAsVerifying()
|
||||
try {
|
||||
if (nip05Client.verify(nip05, hexKey)) {
|
||||
markAsVerified()
|
||||
} else {
|
||||
markAsInvalid()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
markAsError()
|
||||
if (e is CancellationException) throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Stable
|
||||
sealed class Nip05VerifState(
|
||||
val nextTimeToCheck: Long,
|
||||
) {
|
||||
@Stable
|
||||
class Verified(
|
||||
nextTimeToCheck: Long,
|
||||
) : Nip05VerifState(nextTimeToCheck)
|
||||
|
||||
@Stable
|
||||
class Failed(
|
||||
nextTimeToCheck: Long,
|
||||
) : Nip05VerifState(nextTimeToCheck)
|
||||
|
||||
@Stable
|
||||
class Verifying(
|
||||
nextTimeToCheck: Long,
|
||||
) : Nip05VerifState(nextTimeToCheck)
|
||||
|
||||
@Stable
|
||||
class Error(
|
||||
nextTimeToCheck: Long,
|
||||
) : Nip05VerifState(nextTimeToCheck)
|
||||
|
||||
@Stable
|
||||
object NotStarted : Nip05VerifState(0)
|
||||
|
||||
fun isExpired() = TimeUtils.now() > nextTimeToCheck
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
@Stable
|
||||
class UserNip05Cache {
|
||||
val flow: MutableStateFlow<Nip05State> = MutableStateFlow(Nip05State.NotFound)
|
||||
|
||||
fun newMetadata(
|
||||
nip05: String?,
|
||||
ownerPubKey: HexKey,
|
||||
) {
|
||||
if (nip05.isNullOrBlank()) {
|
||||
flow.tryEmit(Nip05State.NotFound)
|
||||
} else {
|
||||
val current = flow.value
|
||||
val parsedNip05 = Nip05Id.parse(nip05)
|
||||
|
||||
if (parsedNip05 == null) {
|
||||
flow.tryEmit(Nip05State.NotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if (current is Nip05State.Exists && current.nip05 == parsedNip05) return
|
||||
|
||||
flow.update { current ->
|
||||
if (current is Nip05State.Exists && current.nip05 == parsedNip05) {
|
||||
current
|
||||
} else {
|
||||
Nip05State.Exists(parsedNip05, ownerPubKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-4
@@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.commons.model.nip28PublicChats
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
@@ -99,8 +99,20 @@ class PublicChatChannel(
|
||||
fun summary(): String? = info.about
|
||||
|
||||
fun profilePicture(): String? {
|
||||
if (info.picture.isNullOrBlank()) return creator?.info?.banner
|
||||
return info.picture ?: creator?.info?.banner
|
||||
if (info.picture.isNullOrBlank()) {
|
||||
return creator
|
||||
?.metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.banner
|
||||
}
|
||||
return info.picture ?: creator
|
||||
?.metadataOrNull()
|
||||
?.flow
|
||||
?.value
|
||||
?.info
|
||||
?.banner
|
||||
}
|
||||
|
||||
fun anyNameStartsWith(prefix: String): Boolean =
|
||||
|
||||
+1
-1
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.commons.richtext
|
||||
import com.linkedin.urls.detection.UrlDetector
|
||||
import com.linkedin.urls.detection.UrlDetectorOptions
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.commons.richtext
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.commons.util
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
|
||||
fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2
|
||||
|
||||
|
||||
+11
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.metadata
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
@@ -37,6 +38,7 @@ import com.vitorpamplona.quartz.nip01Core.metadata.tags.PronounsTag
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.tags.WebsiteTag
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag
|
||||
@@ -62,6 +64,8 @@ class MetadataEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun contactMetadataJson() =
|
||||
try {
|
||||
Json.parseToJsonElement(content) as JsonObject
|
||||
@@ -82,6 +86,13 @@ class MetadataEvent(
|
||||
|
||||
companion object {
|
||||
const val KIND = 0
|
||||
const val FIXED_D_TAG = ""
|
||||
|
||||
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun newUser(
|
||||
name: String?,
|
||||
|
||||
+16
-9
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.quartz.nip01Core.metadata
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -38,21 +37,13 @@ class UserMetadata {
|
||||
var about: String? = null
|
||||
var bot: Boolean? = null
|
||||
var pronouns: String? = null
|
||||
|
||||
var nip05: String? = null
|
||||
var nip05Verified: Boolean = false
|
||||
var nip05LastVerificationTime: Long? = 0
|
||||
|
||||
var domain: String? = null
|
||||
var lud06: String? = null
|
||||
var lud16: String? = null
|
||||
|
||||
var twitter: String? = null
|
||||
|
||||
@kotlinx.serialization.Transient
|
||||
@kotlin.jvm.Transient
|
||||
var tags: ImmutableListOfLists<String>? = null
|
||||
|
||||
fun anyName(): String? = displayName ?: name
|
||||
|
||||
fun anyNameStartsWith(prefix: String): Boolean =
|
||||
@@ -64,6 +55,22 @@ class UserMetadata {
|
||||
|
||||
fun bestName(): String? = displayName ?: name
|
||||
|
||||
fun firstName(): String? {
|
||||
val fullName = bestName() ?: return null
|
||||
|
||||
val names = fullName.split(' ')
|
||||
|
||||
val firstName =
|
||||
if (names[0].length <= 3) {
|
||||
// too short. Remove Dr.
|
||||
"${names[0]} ${names.getOrNull(1) ?: ""}"
|
||||
} else {
|
||||
names[0]
|
||||
}
|
||||
|
||||
return firstName
|
||||
}
|
||||
|
||||
fun nip05(): String? = nip05
|
||||
|
||||
fun profilePicture(): String? = picture
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip05DnsIdentifiers
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
class Nip05 {
|
||||
fun assembleUrl(nip05address: String): String? {
|
||||
val parts = nip05address.trim().split("@")
|
||||
|
||||
if (parts.size == 2) {
|
||||
return "https://${parts[1]}/.well-known/nostr.json?name=${parts[0]}"
|
||||
}
|
||||
if (parts.size == 1) {
|
||||
return "https://${parts[0]}/.well-known/nostr.json?name=_"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun parseHexKeyFor(
|
||||
nip05: String,
|
||||
returnBody: String,
|
||||
): Result<String?> {
|
||||
val parts = nip05.split("@")
|
||||
val user =
|
||||
if (parts.size == 2) {
|
||||
parts[0].lowercase()
|
||||
} else {
|
||||
"_"
|
||||
}
|
||||
|
||||
// NIP05 usernames are case insensitive, but JSON properties are not
|
||||
// converts the json to lowercase and then tries to access the username via a
|
||||
// lowercase version of the username.
|
||||
return try {
|
||||
val rootElement = Json.parseToJsonElement(returnBody.lowercase())
|
||||
val hexKey =
|
||||
rootElement.jsonObject["names"]
|
||||
?.jsonObject[user]
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
Result.success(hexKey)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("Nip05", "Unable to Parse NIP-05 for $nip05 with $returnBody", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip05DnsIdentifiers
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
data class Nip05KeyInfo(
|
||||
val pubkey: HexKey,
|
||||
val relays: List<String>,
|
||||
)
|
||||
|
||||
class Nip05Client(
|
||||
val fetcher: Nip05Fetcher,
|
||||
) {
|
||||
val parser = Nip05Parser()
|
||||
|
||||
suspend fun verify(
|
||||
nip05: Nip05Id,
|
||||
hexKey: HexKey,
|
||||
): Boolean {
|
||||
val json = fetchNip05Data(nip05)
|
||||
|
||||
val key =
|
||||
try {
|
||||
parser.parseHexKey(nip05, json)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
throw IllegalStateException("Error Parsing JSON from NIP-05 address $nip05", e)
|
||||
}
|
||||
|
||||
return if (key == null) {
|
||||
false
|
||||
} else {
|
||||
key == hexKey
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun get(nip05: Nip05Id) = parser.parseHexKeyAndRelays(nip05, fetchNip05Data(nip05))
|
||||
|
||||
suspend fun load(nip05: Nip05Id) = parser.parse(fetchNip05Data(nip05))
|
||||
|
||||
suspend fun list(domain: String) = parser.parse(fetchNip05Data(domain))
|
||||
|
||||
suspend fun fetchNip05Data(nip05: Nip05Id): String {
|
||||
val url = nip05.toUserUrl()
|
||||
|
||||
return try {
|
||||
fetcher.fetch(url)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
throw IllegalStateException("Error Fetching JSON from NIP-05 address $nip05 at $url", e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchNip05Data(domain: String): String {
|
||||
val url = Nip05Id.domainUrl(domain)
|
||||
|
||||
return try {
|
||||
fetcher.fetch(url)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
throw IllegalStateException("Error Fetching JSON from the entire NIP-05 domain $domain at $url", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip05DnsIdentifiers
|
||||
|
||||
interface Nip05Fetcher {
|
||||
suspend fun fetch(url: String): String
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip05DnsIdentifiers
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
data class Nip05Id(
|
||||
val name: String,
|
||||
val domain: String,
|
||||
) {
|
||||
fun toValue(): String = assemble(name, domain)
|
||||
|
||||
fun toUserUrl(): String = userUrl(name, domain)
|
||||
|
||||
fun toDomainUrl(): String = domainUrl(domain)
|
||||
|
||||
companion object {
|
||||
fun parse(nip05address: String): Nip05Id? {
|
||||
val parts = nip05address.trim().lowercase().split("@")
|
||||
|
||||
return when (parts.size) {
|
||||
2 -> Nip05Id(parts[0], parts[1])
|
||||
1 -> Nip05Id(parts[0], "_")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
name: String,
|
||||
domain: String,
|
||||
) = "$name@$domain"
|
||||
|
||||
fun userUrl(
|
||||
name: String,
|
||||
domain: String,
|
||||
) = "https://$domain/.well-known/nostr.json?name=$name"
|
||||
|
||||
fun domainUrl(domain: String) = "https://$domain/.well-known/nostr.json"
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip05DnsIdentifiers
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.utils.text
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
@Serializable
|
||||
data class KeyInfoSet(
|
||||
val names: Map<String, String>,
|
||||
val relays: Map<String, List<String>>,
|
||||
)
|
||||
|
||||
class Nip05Parser {
|
||||
fun toJson(keyInfo: KeyInfoSet) = Json.encodeToString(keyInfo)
|
||||
|
||||
fun parse(json: String) = Json.decodeFromString<KeyInfoSet>(json)
|
||||
|
||||
fun parseHexKey(
|
||||
nip05: Nip05Id,
|
||||
json: String,
|
||||
): HexKey? =
|
||||
Json
|
||||
.parseToJsonElement(json)
|
||||
.jsonObject["names"]
|
||||
?.jsonObject[nip05.name]
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
|
||||
fun parseHexKeyAndRelays(
|
||||
nip05: Nip05Id,
|
||||
json: String,
|
||||
): Nip05KeyInfo? {
|
||||
val rootElement = Json.parseToJsonElement(json)
|
||||
|
||||
val hexKey =
|
||||
rootElement.jsonObject["names"]
|
||||
?.jsonObject[nip05.name]
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
|
||||
return if (!hexKey.isNullOrEmpty()) {
|
||||
val relays =
|
||||
rootElement.jsonObject["relays"]
|
||||
?.jsonObject[hexKey]
|
||||
?.jsonArray
|
||||
?.map {
|
||||
it.jsonPrimitive.text
|
||||
} ?: emptyList()
|
||||
|
||||
Nip05KeyInfo(hexKey, relays)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,8 @@ object TimeUtils {
|
||||
|
||||
fun fiveMinutesAgo() = now() - FIVE_MINUTES
|
||||
|
||||
fun fiveMinutesAhead() = now() + FIVE_MINUTES
|
||||
|
||||
fun fifteenMinutesAgo() = now() - FIFTEEN_MINUTES
|
||||
|
||||
fun oneHourAgo() = now() - ONE_HOUR
|
||||
|
||||
+81
-23
@@ -23,45 +23,66 @@ package com.vitorpamplona.quartz.nip05DnsIdentifiers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.fail
|
||||
|
||||
class Nip05Test {
|
||||
companion object {
|
||||
private val ALL_UPPER_CASE_USER_NAME = "ONETWO"
|
||||
private val ALL_LOWER_CASE_USER_NAME = "onetwo"
|
||||
private const val ALL_UPPER_CASE_USER_NAME = "ONETWO"
|
||||
private const val ALL_LOWER_CASE_USER_NAME = "onetwo"
|
||||
}
|
||||
|
||||
var nip05Verifier = Nip05()
|
||||
val parser = Nip05Parser()
|
||||
val baseTestJson =
|
||||
"""
|
||||
{
|
||||
"names": {
|
||||
"bob": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9"
|
||||
},
|
||||
"relays": {
|
||||
"b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9": [ "wss://relay.example.com", "wss://relay2.example.com" ]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
@Test
|
||||
fun `test with matching case on user name`() =
|
||||
runTest {
|
||||
// Set-up
|
||||
val userNameToTest = ALL_UPPER_CASE_USER_NAME
|
||||
val userNameToTest = ALL_LOWER_CASE_USER_NAME
|
||||
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
|
||||
val nostrJson = "{\n \"names\": {\n \"$userNameToTest\": \"$expectedPubKey\" \n }\n}"
|
||||
val nip05 = "$userNameToTest@domain.com"
|
||||
|
||||
nip05Verifier.parseHexKeyFor(nip05, nostrJson).fold(
|
||||
onSuccess = { assertEquals(expectedPubKey, it) },
|
||||
onFailure = { fail("Test failure") },
|
||||
)
|
||||
val parsedNip05 = Nip05Id.parse(nip05)
|
||||
assertNotNull(parsedNip05)
|
||||
assertEquals(expectedPubKey, parser.parseHexKey(parsedNip05, nostrJson))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test with NOT matching case on user name`() =
|
||||
fun `test failure of lowercase name with uppercase in the json`() =
|
||||
runTest {
|
||||
// Set-up
|
||||
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
|
||||
val nostrJson = "{ \"names\": { \"$ALL_UPPER_CASE_USER_NAME\": \"$expectedPubKey\" }}"
|
||||
|
||||
val nip05 = "$ALL_LOWER_CASE_USER_NAME@domain.com"
|
||||
val parsedNip05 = Nip05Id.parse(nip05)
|
||||
assertNotNull(parsedNip05)
|
||||
assertEquals(null, parser.parseHexKey(parsedNip05, nostrJson))
|
||||
}
|
||||
|
||||
nip05Verifier.parseHexKeyFor(nip05, nostrJson).fold(
|
||||
onSuccess = { assertEquals(expectedPubKey, it) },
|
||||
onFailure = { fail("Test failure") },
|
||||
)
|
||||
@Test
|
||||
fun `test uppercase name with lowercase name in the json`() =
|
||||
runTest {
|
||||
// Set-up
|
||||
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
|
||||
val nostrJson = "{ \"names\": { \"$ALL_LOWER_CASE_USER_NAME\": \"$expectedPubKey\" }}"
|
||||
|
||||
val nip05 = "$ALL_UPPER_CASE_USER_NAME@domain.com"
|
||||
val parsedNip05 = Nip05Id.parse(nip05)
|
||||
assertNotNull(parsedNip05)
|
||||
assertEquals(expectedPubKey, parser.parseHexKey(parsedNip05, nostrJson))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -69,11 +90,8 @@ class Nip05Test {
|
||||
// given
|
||||
val nip05address = "this@that@that.com"
|
||||
|
||||
// when
|
||||
val actualValue = nip05Verifier.assembleUrl(nip05address)
|
||||
|
||||
// then
|
||||
assertNull(actualValue)
|
||||
val parsedNip05 = Nip05Id.parse(nip05address)
|
||||
assertNull(parsedNip05)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,12 +100,52 @@ class Nip05Test {
|
||||
val userName = "TheUser"
|
||||
val domain = "domain.com"
|
||||
val nip05address = "$userName@$domain"
|
||||
val expectedValue = "https://$domain/.well-known/nostr.json?name=$userName"
|
||||
|
||||
// when
|
||||
val actualValue = nip05Verifier.assembleUrl(nip05address)
|
||||
val parsedNip05 = Nip05Id.parse(nip05address)
|
||||
assertNotNull(parsedNip05)
|
||||
|
||||
// then
|
||||
assertEquals(expectedValue, actualValue)
|
||||
assertEquals("https://$domain/.well-known/nostr.json?name=${userName.lowercase()}", parsedNip05.toUserUrl())
|
||||
assertEquals("https://$domain/.well-known/nostr.json", parsedNip05.toDomainUrl())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test json parsing with relays`() {
|
||||
val parsedNip05 = Nip05Id.parse("bob@test.com")
|
||||
assertNotNull(parsedNip05)
|
||||
|
||||
val result = parser.parseHexKeyAndRelays(parsedNip05, baseTestJson)
|
||||
assertNotNull(result)
|
||||
assertEquals("b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", result.pubkey)
|
||||
assertEquals(
|
||||
listOf("wss://relay.example.com", "wss://relay2.example.com"),
|
||||
result.relays,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test full json parsing with relays`() {
|
||||
val result = parser.parse(baseTestJson)
|
||||
|
||||
assertNotNull(result)
|
||||
|
||||
assertEquals(1, result.names.size)
|
||||
assertEquals(1, result.relays.size)
|
||||
|
||||
assertEquals("bob", result.names.keys.first())
|
||||
assertEquals("b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", result.names["bob"])
|
||||
assertEquals("b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", result.relays.keys.first())
|
||||
assertEquals(
|
||||
listOf("wss://relay.example.com", "wss://relay2.example.com"),
|
||||
result.relays["b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9"],
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test full json generation with relays`() {
|
||||
val result = parser.parse(baseTestJson)
|
||||
val newResult = parser.parse(parser.toJson(result))
|
||||
|
||||
assertEquals(result, newResult)
|
||||
}
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip05DnsIdentifiers
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
|
||||
class OkHttpNip05Fetcher(
|
||||
val okHttpClient: (String) -> OkHttpClient,
|
||||
) : Nip05Fetcher {
|
||||
override suspend fun fetch(url: String): String =
|
||||
withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder().url(url).build()
|
||||
|
||||
// Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint.
|
||||
val client = okHttpClient(url).newBuilder().followRedirects(false).build()
|
||||
|
||||
client.newCall(request).executeAsync().use { response ->
|
||||
withContext(Dispatchers.IO) {
|
||||
if (response.isSuccessful) {
|
||||
response.body.string()
|
||||
} else {
|
||||
throw IllegalStateException("Error: ${response.code}, ${response.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user