From 7bc726575743b115dd4400ab3946b37f32cac5dd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 4 Feb 2026 14:31:30 -0500 Subject: [PATCH] Refactors the old NIP-05 code on Quartz New Caching system for User metadata New Caching system for NIP-05 verifications --- .../amethyst/UrlUserTagTransformationTest.kt | 47 +++- .../com/vitorpamplona/amethyst/AppModules.kt | 6 + .../com/vitorpamplona/amethyst/DebugUtils.kt | 2 +- .../amethyst/model/HashtagIcon.kt | 2 +- .../amethyst/model/LocalCache.kt | 27 +- .../nip01UserMetadata/UserMetadataState.kt | 17 +- .../amethyst/service/CachedRichTextParser.kt | 2 +- .../amethyst/service/EmojiUtils.kt | 2 +- .../service/Nip05NostrAddressVerifier.kt | 94 ------- .../amethyst/service/ZapPaymentHandler.kt | 13 +- .../compose/NotifyRequestDialog.kt | 2 +- .../reqCommand/user/UserObservers.kt | 139 ++++------ .../user/watchers/FilterUserMetadataForKey.kt | 2 +- .../amethyst/ui/actions/EditPostView.kt | 45 ++-- .../amethyst/ui/actions/EditPostViewModel.kt | 2 +- .../ui/actions/NewUserMetadataViewModel.kt | 33 ++- .../amethyst/ui/components/ClickableRoute.kt | 7 +- .../ui/components/ExpandableRichTextViewer.kt | 2 +- .../amethyst/ui/components/RichTextViewer.kt | 6 +- .../markdown/RenderContentAsMarkdown.kt | 4 +- .../components/toasts/multiline/ErrorList.kt | 2 +- .../drawer/AccountSwitchBottomSheet.kt | 7 +- .../ui/navigation/drawer/DrawerContent.kt | 8 +- .../amethyst/ui/note/MultiSetCompose.kt | 2 +- .../ui/note/NIP05VerificationDisplay.kt | 240 ++++++++---------- .../amethyst/ui/note/NoteCompose.kt | 1 - .../amethyst/ui/note/PollNote.kt | 6 +- .../amethyst/ui/note/ReplyInformation.kt | 2 +- .../amethyst/ui/note/UserProfilePicture.kt | 27 +- .../amethyst/ui/note/UsernameDisplay.kt | 4 +- .../creators/invoice/NewPostInvoiceRequest.kt | 10 +- .../ui/note/creators/notify/Notifying.kt | 10 +- .../note/elements/DisplayUncitedHashtags.kt | 2 +- .../amethyst/ui/note/elements/ForkInfo.kt | 25 +- .../nip22Comments/CommentPostViewModel.kt | 10 +- .../amethyst/ui/note/types/AppDefinition.kt | 4 +- .../amethyst/ui/note/types/AudioTrack.kt | 2 +- .../amethyst/ui/note/types/CommunityHeader.kt | 4 +- .../amethyst/ui/note/types/FollowList.kt | 4 +- .../amethyst/ui/note/types/Git.kt | 4 +- .../amethyst/ui/note/types/Highlight.kt | 4 +- .../ui/note/types/InteractiveStory.kt | 2 +- .../types/NIP90ContentDiscoveryResponse.kt | 4 +- .../amethyst/ui/note/types/PictureDisplay.kt | 2 +- .../amethyst/ui/note/types/PinList.kt | 2 +- .../amethyst/ui/note/types/Poll.kt | 4 +- .../amethyst/ui/note/types/PrivateMessage.kt | 4 +- .../amethyst/ui/note/types/PublicMessage.kt | 2 +- .../amethyst/ui/note/types/Report.kt | 2 +- .../amethyst/ui/note/types/Text.kt | 4 +- .../ui/note/types/TextModification.kt | 2 +- .../amethyst/ui/note/types/Video.kt | 4 +- .../ui/screen/AccountStateViewModel.kt | 28 +- .../ui/screen/loggedIn/AccountViewModel.kt | 51 +--- .../loggedIn/chats/feed/DrawAuthorInfo.kt | 8 +- .../types/RenderChangeChannelMetadataNote.kt | 2 +- .../feed/types/RenderCreateChannelNote.kt | 6 +- .../chats/feed/types/RenderEncryptedFile.kt | 2 +- .../chats/feed/types/RenderRegularTextNote.kt | 4 +- .../privateDM/datasource/FilterNip04DMs.kt | 8 +- .../privateDM/header/RoomNameOnlyDisplay.kt | 13 +- .../privateDM/send/ChatNewMessageViewModel.kt | 6 +- .../header/LongLiveActivityChannelHeader.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 6 +- .../nip99Classifieds/NewProductViewModel.kt | 6 +- .../loggedIn/home/ShortNotePostViewModel.kt | 12 +- .../loggedIn/notifications/CardFeedState.kt | 2 +- .../NewPublicMessageViewModel.kt | 6 +- .../profile/header/DrawAdditionalInfo.kt | 108 +++++--- .../loggedIn/profile/header/ProfileHeader.kt | 61 +++-- .../ui/screen/loggedIn/qrcode/ShowQRScreen.kt | 146 ++++++----- .../loggedIn/threadview/ThreadFeedView.kt | 12 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 2 +- .../components/TranslatableRichTextViewer.kt | 2 +- .../benchmark/RichTextParserBenchmark.kt | 4 +- .../commons/richtext/GalleryParserTest.kt | 2 +- .../commons/richtext/RichTextParserTest.kt | 4 +- .../commons/model}/ImmutableListOfLists.kt | 2 +- .../amethyst/commons/model/Note.kt | 8 +- .../amethyst/commons/model/User.kt | 158 +++++++----- .../model/nip01Core/UserMetadataCache.kt | 62 +++++ .../model/nip05DnsIdentifiers/Nip05State.kt | 70 +++++ .../nip05DnsIdentifiers/Nip05VerifState.kt | 54 ++++ .../nip05DnsIdentifiers/UserNip05Cache.kt | 59 +++++ .../nip28PublicChats/PublicChatChannel.kt | 20 +- .../commons/richtext/RichTextParser.kt | 2 +- .../richtext/RichTextParserSegments.kt | 2 +- .../amethyst/commons/util/EmojiUtils.kt | 2 +- .../nip01Core/metadata/MetadataEvent.kt | 11 + .../quartz/nip01Core/metadata/UserMetadata.kt | 25 +- .../quartz/nip05DnsIdentifiers/Nip05.kt | 72 ------ .../quartz/nip05DnsIdentifiers/Nip05Client.kt | 84 ++++++ .../nip05DnsIdentifiers/Nip05Fetcher.kt | 25 ++ .../quartz/nip05DnsIdentifiers/Nip05Id.kt | 59 +++++ .../quartz/nip05DnsIdentifiers/Nip05Parser.kt | 79 ++++++ .../vitorpamplona/quartz/utils/TimeUtils.kt | 2 + .../quartz/nip05DnsIdentifiers/Nip05Test.kt | 104 ++++++-- .../nip05DnsIdentifiers/OkHttpNip05Fetcher.kt | 49 ++++ 98 files changed, 1425 insertions(+), 891 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt rename {quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model}/ImmutableListOfLists.kt (96%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserMetadataCache.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/Nip05State.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/Nip05VerifState.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/UserNip05Cache.kt delete mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Client.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Fetcher.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Parser.kt create mode 100644 quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/OkHttpNip05Fetcher.kt diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt index 16945e37c..d92f468d2 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/UrlUserTagTransformationTest.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 8f28543b3..553de64e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -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() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 84277731f..ffe4c9cc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -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(), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt index f9afce5bc..9d3c55133 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index a636b1068..b9da98618 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt index 503eb1c4c..ebe119d81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt @@ -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 = 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) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index 792efcb8a..d1b9212ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -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(50) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt index 543cd8fce..5a3812228 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt deleted file mode 100644 index 5ad6597e8..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt +++ /dev/null @@ -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, - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index 42adc194f..78528fb04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -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) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt index b61ab0ea8..dcfa156f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 7ad81ba07..2e0c03e88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -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 { - // 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 { - // 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 { +): State { // 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 { - // 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt index 2f634dbcf..2c680cbe4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 8387333de..577f03f63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -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) }, + ) + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 0018215d9..758530ff7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -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 ?: "") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 32dddea67..158b7504f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -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() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 1d4264585..43baf9164 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -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) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 9b1f191f3..95541c1a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -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(10) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index c0ca8c5a0..d9b6edca2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt index 9f20f4687..5c77a3bbd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt index 5950caf61..bf7a6aed1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt index cda6c74fb..9bce2e92c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index 3ae138470..8d0addbf1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 100db2569..7054706f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 48ad80fbf..e94ac7918 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -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 { - 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, 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, +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, +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, - ) - } - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 84cb7b979..97e1f1dea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 263b563c7..516d97de9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt index d3392b4f4..10697c5eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 06a62f2f9..89a91482f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index 0c8a4a4ef..b6a15c2d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -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 { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt index a2e04533e..cae86a5c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt index 519ebea28..a0d471e04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/Notifying.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt index a7601f1ef..6472160eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index b36093573..d181ca48b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -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, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 0b634d26a..bdb2d0e3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -176,11 +176,11 @@ open class CommentPostViewModel : var wantsZapraiser by mutableStateOf(false) override val zapRaiserAmount = mutableStateOf(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" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index 85074bd44..8f8567835 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index cb37c17cc..1cc72a4f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 115431784..f214401e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt index c79454a9d..051e4395c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index 52ba58254..23caf3e3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index f20b58ed8..819cba03e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt index 08afdcf5b..01814869c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt index 734c2fde2..b6854a1c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt index 6b0f27e35..a2b483176 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt index afc871c08..133f9a091 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index bf0a07f68..666cf9add 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index 583a22ab7..b6904da3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt index 78f0fedd1..ca529587c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt index 5a861de6a..c4f3a6e76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 76c68e29b..76505297c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt index efa188f01..924b0086f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index b8c26a7be..d5c60b321 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index c969c26e4..849afc8b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -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) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index fffbe41a2..363f60ab0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -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 { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt index bd2a75c42..208af7658 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt index 84e9a7d29..2f0ebbbed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt index 34c7600c2..cf0bc07bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt index f48d0b3af..bf56dc3fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt index f704649b7..2534e250a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt index 9f4c0fa90..9901ff38d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt @@ -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) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt index e5f0a008f..9388f5744 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index eabc0d274..cd0146a58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index 9ad0b1e88..8f5aad376 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index d0b2de1ab..3e14072d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -155,11 +155,11 @@ open class ChannelNewMessageViewModel : var wantsZapraiser by mutableStateOf(false) var zapRaiserAmount by mutableStateOf(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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index af0b07c96..0e48d916f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -169,11 +169,11 @@ open class NewProductViewModel : var relayList by mutableStateOf?>(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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 90ee62b3a..87f152e12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -260,9 +260,9 @@ open class ShortNotePostViewModel : var wantsZapRaiser by mutableStateOf(false) override val zapRaiserAmount = mutableStateOf(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" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt index 7f696f9bf..0c71feb37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index b44ac347b..2fbefd6e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -178,11 +178,11 @@ class NewPublicMessageViewModel : var wantsZapraiser by mutableStateOf(false) override var zapRaiserAmount = mutableStateOf(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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 6f4a4dc8b..33475090a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt index 5c9b35109..1f92e1e68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt @@ -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(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, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt index 89da19efa..ff68d9b58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt @@ -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, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 3ed04553f..13422cf9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -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) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index 225559dce..149c74bae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -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 diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index f71a7587c..c8b20fb2f 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -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 diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt index 87e971712..0bdbe2fd5 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt @@ -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 diff --git a/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt b/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt index d49ad783c..222fb64a8 100644 --- a/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt +++ b/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParserTest.kt @@ -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 diff --git a/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt b/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt index 2bf2ea3f6..f76867283 100644 --- a/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt +++ b/commons/src/androidTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt @@ -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 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/ImmutableListOfLists.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ImmutableListOfLists.kt similarity index 96% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/ImmutableListOfLists.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ImmutableListOfLists.kt index bf0b29d39..a910a3957 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/ImmutableListOfLists.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ImmutableListOfLists.kt @@ -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 diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt index 5ef037053..d32d945c0 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt @@ -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 = diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt index 558cf168c..395719ce2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt @@ -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, UserDependencies>() - - var info: UserMetadata? = null - - var latestMetadata: MetadataEvent? = null - var latestMetadataRelay: NormalizedRelayUrl? = null var latestContactList: ContactListEvent? = null var zaps = mapOf() @@ -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 { 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): 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() || diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserMetadataCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserMetadataCache.kt new file mode 100644 index 000000000..7415cb940 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip01Core/UserMetadataCache.kt @@ -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, + val identities: List, + val createdAt: Long, +) + +@Stable +class UserMetadataCache { + val flow: MutableStateFlow = 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) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/Nip05State.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/Nip05State.kt new file mode 100644 index 000000000..7cb2bcc27 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/Nip05State.kt @@ -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.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 + } + } + } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/Nip05VerifState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/Nip05VerifState.kt new file mode 100644 index 000000000..bc62108ec --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/Nip05VerifState.kt @@ -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 +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/UserNip05Cache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/UserNip05Cache.kt new file mode 100644 index 000000000..68a7ee2ba --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip05DnsIdentifiers/UserNip05Cache.kt @@ -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 = 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) + } + } + } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatChannel.kt index 3e9b04b38..445a70174 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatChannel.kt @@ -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 = diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index 70d90061d..5d60cb904 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -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 diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt index f826220c6..5a284ba98 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt @@ -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 diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EmojiUtils.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EmojiUtils.kt index 8b677f5ed..97c5fa2d6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EmojiUtils.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EmojiUtils.kt @@ -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 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt index c9fb6784e..c8c102a6b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt @@ -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?, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt index 2499c8379..88c5e2445 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UserMetadata.kt @@ -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? = 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 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05.kt deleted file mode 100644 index c390be19f..000000000 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05.kt +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Client.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Client.kt new file mode 100644 index 000000000..efe3a2d78 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Client.kt @@ -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, +) + +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) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Fetcher.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Fetcher.kt new file mode 100644 index 000000000..1b6af73be --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Fetcher.kt @@ -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 +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt new file mode 100644 index 000000000..48d8a7839 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Id.kt @@ -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" + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Parser.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Parser.kt new file mode 100644 index 000000000..9b16a0144 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Parser.kt @@ -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, + val relays: Map>, +) + +class Nip05Parser { + fun toJson(keyInfo: KeyInfoSet) = Json.encodeToString(keyInfo) + + fun parse(json: String) = Json.decodeFromString(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 + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/TimeUtils.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/TimeUtils.kt index 565a60862..23882adf5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/TimeUtils.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/TimeUtils.kt @@ -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 diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Test.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Test.kt index dc480e4bf..85147b154 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Test.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05Test.kt @@ -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) } } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/OkHttpNip05Fetcher.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/OkHttpNip05Fetcher.kt new file mode 100644 index 000000000..baa06ac7d --- /dev/null +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/OkHttpNip05Fetcher.kt @@ -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}") + } + } + } + } +}