feat: add NIP-39 kind 10011 (ExternalIdentitiesEvent) support
- Add ExternalIdentitiesEvent (kind 10011) to quartz/nip39ExtIdentities with createNew/updateFromPast factory methods and identityClaims() - Generalize TagArrayBuilderExt to work with any Event type (not just MetadataEvent) so claims/twitterClaim etc. work with both kinds - Extract replaceClaims logic to List<IdentityClaimTag>.replaceClaims() shared extension used by both MetadataEvent and ExternalIdentitiesEvent - Register ExternalIdentitiesEvent in EventFactory (kind 10011) - Add consume(ExternalIdentitiesEvent) to LocalCache and dispatch in justConsumeInnerInner - Add ExternalIdentitiesEvent.KIND to UserMetadataForKeyKinds relay filter so kind 10011 is fetched alongside kind 0 when loading user profiles - Add UserExternalIdentitiesViewModel that observes the kind 10011 AddressableNote for a user and exposes List<IdentityClaimTag> as a Flow - Thread UserExternalIdentitiesViewModel through ProfileScreen → RenderScreen → ProfileHeader → DrawAdditionalInfo - DrawAdditionalInfo now shows kind 10011 identities, falling back to kind 0 for backwards compatibility - UserMetadataState: add sendNewUserIdentities() for kind 10011 and remove identity claim params from sendNewUserMetadata() (kind 0) - NewUserMetadataViewModel: load identities from kind 10011 first (fallback to kind 0), save identities to kind 10011 separately https://claude.ai/code/session_017iU6ZdRu5qRXPCeDztVeeV
This commit is contained in:
@@ -132,6 +132,7 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
|||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||||
|
import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent
|
||||||
import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore
|
import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore
|
||||||
import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||||
@@ -560,6 +561,12 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(
|
||||||
|
event: ExternalIdentitiesEvent,
|
||||||
|
relay: NormalizedRelayUrl?,
|
||||||
|
wasVerified: Boolean,
|
||||||
|
) = consumeBaseReplaceable(event, relay, wasVerified)
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: ContactListEvent,
|
event: ContactListEvent,
|
||||||
relay: NormalizedRelayUrl?,
|
relay: NormalizedRelayUrl?,
|
||||||
@@ -3050,6 +3057,7 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
is EmojiPackSelectionEvent -> consume(event, relay, wasVerified)
|
is EmojiPackSelectionEvent -> consume(event, relay, wasVerified)
|
||||||
is EphemeralChatEvent -> consume(event, relay, wasVerified)
|
is EphemeralChatEvent -> consume(event, relay, wasVerified)
|
||||||
is EphemeralChatListEvent -> consume(event, relay, wasVerified)
|
is EphemeralChatListEvent -> consume(event, relay, wasVerified)
|
||||||
|
is ExternalIdentitiesEvent -> consume(event, relay, wasVerified)
|
||||||
is GenericRepostEvent -> consume(event, relay, wasVerified)
|
is GenericRepostEvent -> consume(event, relay, wasVerified)
|
||||||
is FhirResourceEvent -> consume(event, relay, wasVerified)
|
is FhirResourceEvent -> consume(event, relay, wasVerified)
|
||||||
is FileHeaderEvent -> consume(event, relay, wasVerified)
|
is FileHeaderEvent -> consume(event, relay, wasVerified)
|
||||||
|
|||||||
+30
-6
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.AccountSettings
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
@@ -51,6 +52,12 @@ class UserMetadataState(
|
|||||||
|
|
||||||
fun getUserMetadataEvent(): MetadataEvent? = note.event as? MetadataEvent
|
fun getUserMetadataEvent(): MetadataEvent? = note.event as? MetadataEvent
|
||||||
|
|
||||||
|
fun getIdentitiesEventAddress() = ExternalIdentitiesEvent.createAddress(signer.pubKey)
|
||||||
|
|
||||||
|
val identitiesNote = cache.getOrCreateAddressableNote(getIdentitiesEventAddress())
|
||||||
|
|
||||||
|
fun getExternalIdentitiesEvent(): ExternalIdentitiesEvent? = identitiesNote.event as? ExternalIdentitiesEvent
|
||||||
|
|
||||||
suspend fun sendNewUserMetadata(
|
suspend fun sendNewUserMetadata(
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
displayName: String? = null,
|
displayName: String? = null,
|
||||||
@@ -62,9 +69,6 @@ class UserMetadataState(
|
|||||||
nip05: String? = null,
|
nip05: String? = null,
|
||||||
lnAddress: String? = null,
|
lnAddress: String? = null,
|
||||||
lnURL: String? = null,
|
lnURL: String? = null,
|
||||||
twitter: String? = null,
|
|
||||||
mastodon: String? = null,
|
|
||||||
github: String? = null,
|
|
||||||
): MetadataEvent {
|
): MetadataEvent {
|
||||||
val latest = getUserMetadataEvent()
|
val latest = getUserMetadataEvent()
|
||||||
|
|
||||||
@@ -82,9 +86,6 @@ class UserMetadataState(
|
|||||||
nip05 = nip05,
|
nip05 = nip05,
|
||||||
lnAddress = lnAddress,
|
lnAddress = lnAddress,
|
||||||
lnURL = lnURL,
|
lnURL = lnURL,
|
||||||
twitter = twitter,
|
|
||||||
mastodon = mastodon,
|
|
||||||
github = github,
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MetadataEvent.createNew(
|
MetadataEvent.createNew(
|
||||||
@@ -98,6 +99,29 @@ class UserMetadataState(
|
|||||||
nip05 = nip05,
|
nip05 = nip05,
|
||||||
lnAddress = lnAddress,
|
lnAddress = lnAddress,
|
||||||
lnURL = lnURL,
|
lnURL = lnURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer.sign(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendNewUserIdentities(
|
||||||
|
twitter: String? = null,
|
||||||
|
mastodon: String? = null,
|
||||||
|
github: String? = null,
|
||||||
|
): ExternalIdentitiesEvent {
|
||||||
|
val latest = getExternalIdentitiesEvent()
|
||||||
|
|
||||||
|
val template =
|
||||||
|
if (latest != null) {
|
||||||
|
ExternalIdentitiesEvent.updateFromPast(
|
||||||
|
latest = latest,
|
||||||
|
twitter = twitter,
|
||||||
|
mastodon = mastodon,
|
||||||
|
github = github,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ExternalIdentitiesEvent.createNew(
|
||||||
twitter = twitter,
|
twitter = twitter,
|
||||||
mastodon = mastodon,
|
mastodon = mastodon,
|
||||||
github = github,
|
github = github,
|
||||||
|
|||||||
+2
@@ -30,12 +30,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||||
|
import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||||
|
|
||||||
val UserMetadataForKeyKinds =
|
val UserMetadataForKeyKinds =
|
||||||
listOf(
|
listOf(
|
||||||
MetadataEvent.KIND,
|
MetadataEvent.KIND,
|
||||||
|
ExternalIdentitiesEvent.KIND,
|
||||||
StatusEvent.KIND,
|
StatusEvent.KIND,
|
||||||
AdvertisedRelayListEvent.KIND,
|
AdvertisedRelayListEvent.KIND,
|
||||||
ChatMessageRelayListEvent.KIND,
|
ChatMessageRelayListEvent.KIND,
|
||||||
|
|||||||
+21
-10
@@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
|
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
|
||||||
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
|
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
|
||||||
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
|
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
|
||||||
|
import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
class NewUserMetadataViewModel : ViewModel() {
|
class NewUserMetadataViewModel : ViewModel() {
|
||||||
@@ -82,18 +83,23 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
nip05.value = it.info.nip05 ?: ""
|
nip05.value = it.info.nip05 ?: ""
|
||||||
lnAddress.value = it.info.lud16 ?: ""
|
lnAddress.value = it.info.lud16 ?: ""
|
||||||
lnURL.value = it.info.lud06 ?: ""
|
lnURL.value = it.info.lud06 ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
twitter.value = ""
|
twitter.value = ""
|
||||||
github.value = ""
|
github.value = ""
|
||||||
mastodon.value = ""
|
mastodon.value = ""
|
||||||
|
|
||||||
// TODO: Validate Telegram input, somehow.
|
// Load identities from kind 10011 first, fall back to kind 0 for backwards compat
|
||||||
it.identities.forEach { identity ->
|
val identities =
|
||||||
when (identity) {
|
account.userMetadata.getExternalIdentitiesEvent()?.identityClaims()
|
||||||
is TwitterIdentity -> twitter.value = identity.toProofUrl()
|
?: account.userProfile().metadataOrNull()?.flow?.value?.identities
|
||||||
is GitHubIdentity -> github.value = identity.toProofUrl()
|
?: emptyList()
|
||||||
is MastodonIdentity -> mastodon.value = identity.toProofUrl()
|
|
||||||
}
|
identities.forEach { identity ->
|
||||||
|
when (identity) {
|
||||||
|
is TwitterIdentity -> twitter.value = identity.toProofUrl()
|
||||||
|
is GitHubIdentity -> github.value = identity.toProofUrl()
|
||||||
|
is MastodonIdentity -> mastodon.value = identity.toProofUrl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,12 +117,17 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
nip05 = nip05.value,
|
nip05 = nip05.value,
|
||||||
lnAddress = lnAddress.value,
|
lnAddress = lnAddress.value,
|
||||||
lnURL = lnURL.value,
|
lnURL = lnURL.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
val identities =
|
||||||
|
account.userMetadata.sendNewUserIdentities(
|
||||||
twitter = twitter.value,
|
twitter = twitter.value,
|
||||||
mastodon = mastodon.value,
|
mastodon = mastodon.value,
|
||||||
github = github.value,
|
github = github.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
account.sendLiterallyEverywhere(metadata)
|
account.sendLiterallyEverywhere(metadata)
|
||||||
|
account.sendLiterallyEverywhere(identities)
|
||||||
|
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags.FollowedTa
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags.TabFollowedTags
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags.TabFollowedTags
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.ProfileHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.ProfileHeader
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.identity.UserExternalIdentitiesViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.TabMutualConversations
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.TabMutualConversations
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.TabNotesNewThreads
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.TabNotesNewThreads
|
||||||
@@ -169,6 +170,12 @@ fun PrepareViewModels(
|
|||||||
factory = UserAppRecommendationsFeedViewModel.Factory(baseUser),
|
factory = UserAppRecommendationsFeedViewModel.Factory(baseUser),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val externalIdentities: UserExternalIdentitiesViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = baseUser.pubkeyHex + "UserExternalIdentitiesViewModel",
|
||||||
|
factory = UserExternalIdentitiesViewModel.Factory(baseUser),
|
||||||
|
)
|
||||||
|
|
||||||
val zapFeedViewModel: UserProfileZapsViewModel =
|
val zapFeedViewModel: UserProfileZapsViewModel =
|
||||||
viewModel(
|
viewModel(
|
||||||
key = baseUser.pubkeyHex + "UserProfileZapsFeedViewModel",
|
key = baseUser.pubkeyHex + "UserProfileZapsFeedViewModel",
|
||||||
@@ -233,6 +240,7 @@ fun PrepareViewModels(
|
|||||||
followsFeedViewModel,
|
followsFeedViewModel,
|
||||||
followersFeedViewModel,
|
followersFeedViewModel,
|
||||||
appRecommendations,
|
appRecommendations,
|
||||||
|
externalIdentities,
|
||||||
zapFeedViewModel,
|
zapFeedViewModel,
|
||||||
bookmarksFeedViewModel,
|
bookmarksFeedViewModel,
|
||||||
galleryFeedViewModel,
|
galleryFeedViewModel,
|
||||||
@@ -251,6 +259,7 @@ fun ProfileScreen(
|
|||||||
followsFeedViewModel: UserProfileFollowsUserFeedViewModel,
|
followsFeedViewModel: UserProfileFollowsUserFeedViewModel,
|
||||||
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
||||||
appRecommendations: UserAppRecommendationsFeedViewModel,
|
appRecommendations: UserAppRecommendationsFeedViewModel,
|
||||||
|
externalIdentities: UserExternalIdentitiesViewModel,
|
||||||
zapFeedViewModel: UserProfileZapsViewModel,
|
zapFeedViewModel: UserProfileZapsViewModel,
|
||||||
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
||||||
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
||||||
@@ -277,6 +286,7 @@ fun ProfileScreen(
|
|||||||
repliesViewModel,
|
repliesViewModel,
|
||||||
mutualViewModel,
|
mutualViewModel,
|
||||||
appRecommendations,
|
appRecommendations,
|
||||||
|
externalIdentities,
|
||||||
followsFeedViewModel,
|
followsFeedViewModel,
|
||||||
followersFeedViewModel,
|
followersFeedViewModel,
|
||||||
zapFeedViewModel,
|
zapFeedViewModel,
|
||||||
@@ -370,6 +380,7 @@ private fun RenderScreen(
|
|||||||
repliesViewModel: UserProfileConversationsFeedViewModel,
|
repliesViewModel: UserProfileConversationsFeedViewModel,
|
||||||
mutualViewModel: UserProfileMutualFeedViewModel,
|
mutualViewModel: UserProfileMutualFeedViewModel,
|
||||||
appRecommendations: UserAppRecommendationsFeedViewModel,
|
appRecommendations: UserAppRecommendationsFeedViewModel,
|
||||||
|
externalIdentities: UserExternalIdentitiesViewModel,
|
||||||
followsFeedViewModel: UserProfileFollowsUserFeedViewModel,
|
followsFeedViewModel: UserProfileFollowsUserFeedViewModel,
|
||||||
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
||||||
zapFeedViewModel: UserProfileZapsViewModel,
|
zapFeedViewModel: UserProfileZapsViewModel,
|
||||||
@@ -382,7 +393,7 @@ private fun RenderScreen(
|
|||||||
val pagerState = rememberPagerState { 11 }
|
val pagerState = rememberPagerState { 11 }
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
ProfileHeader(baseUser, appRecommendations, nav, accountViewModel)
|
ProfileHeader(baseUser, appRecommendations, externalIdentities, nav, accountViewModel)
|
||||||
ScrollableTabRow(
|
ScrollableTabRow(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
|||||||
+6
-2
@@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges.DisplayBadges
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges.DisplayBadges
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.identity.UserExternalIdentitiesViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||||
@@ -81,6 +82,7 @@ private const val IDENTITY_ICON_CACHE_KEY = 0
|
|||||||
fun DrawAdditionalInfo(
|
fun DrawAdditionalInfo(
|
||||||
baseUser: User,
|
baseUser: User,
|
||||||
appRecommendations: UserAppRecommendationsFeedViewModel,
|
appRecommendations: UserAppRecommendationsFeedViewModel,
|
||||||
|
externalIdentities: UserExternalIdentitiesViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
@@ -88,6 +90,7 @@ fun DrawAdditionalInfo(
|
|||||||
val user = userState ?: return
|
val user = userState ?: return
|
||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
val identities by externalIdentities.identities.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 7.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 7.dp)) {
|
||||||
CreateTextWithEmoji(
|
CreateTextWithEmoji(
|
||||||
@@ -183,8 +186,9 @@ fun DrawAdditionalInfo(
|
|||||||
}
|
}
|
||||||
DisplayLNAddress(lud16, baseUser, accountViewModel, nav)
|
DisplayLNAddress(lud16, baseUser, accountViewModel, nav)
|
||||||
|
|
||||||
if (user.identities.isNotEmpty()) {
|
val displayIdentities = identities.ifEmpty { user.identities }
|
||||||
user.identities.forEach { identity: IdentityClaimTag ->
|
if (displayIdentities.isNotEmpty()) {
|
||||||
|
displayIdentities.forEach { identity: IdentityClaimTag ->
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
|
|||||||
+3
-1
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
|||||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.identity.UserExternalIdentitiesViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size100dp
|
import com.vitorpamplona.amethyst.ui.theme.Size100dp
|
||||||
@@ -82,6 +83,7 @@ import kotlinx.coroutines.launch
|
|||||||
fun ProfileHeader(
|
fun ProfileHeader(
|
||||||
baseUser: User,
|
baseUser: User,
|
||||||
appRecommendations: UserAppRecommendationsFeedViewModel,
|
appRecommendations: UserAppRecommendationsFeedViewModel,
|
||||||
|
externalIdentities: UserExternalIdentitiesViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
@@ -154,7 +156,7 @@ fun ProfileHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawAdditionalInfo(baseUser, appRecommendations, accountViewModel, nav)
|
DrawAdditionalInfo(baseUser, appRecommendations, externalIdentities, accountViewModel, nav)
|
||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(top = 6.dp))
|
HorizontalDivider(modifier = Modifier.padding(top = 6.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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.ui.screen.loggedIn.profile.header.identity
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent
|
||||||
|
import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag
|
||||||
|
import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class UserExternalIdentitiesViewModel(
|
||||||
|
val user: User,
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _identities = MutableStateFlow<List<IdentityClaimTag>>(emptyList())
|
||||||
|
val identities = _identities.asStateFlow()
|
||||||
|
|
||||||
|
private val note =
|
||||||
|
LocalCache.getOrCreateAddressableNote(
|
||||||
|
ExternalIdentitiesEvent.createAddress(user.pubkeyHex),
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
note.flow().metadata.stateFlow.collect { state ->
|
||||||
|
val event = state.note.event as? ExternalIdentitiesEvent
|
||||||
|
_identities.value = event?.identityClaims() ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(
|
||||||
|
val user: User,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserExternalIdentitiesViewModel(user) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-4
@@ -24,16 +24,17 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
|||||||
|
|
||||||
fun MetadataEvent.identityClaims() = tags.mapNotNull(IdentityClaimTag::parse)
|
fun MetadataEvent.identityClaims() = tags.mapNotNull(IdentityClaimTag::parse)
|
||||||
|
|
||||||
fun MetadataEvent.replaceClaims(
|
fun ExternalIdentitiesEvent.identityClaims() = tags.mapNotNull(IdentityClaimTag::parse)
|
||||||
|
|
||||||
|
fun List<IdentityClaimTag>.replaceClaims(
|
||||||
twitter: String?,
|
twitter: String?,
|
||||||
mastodon: String?,
|
mastodon: String?,
|
||||||
github: String?,
|
github: String?,
|
||||||
): List<IdentityClaimTag> {
|
): List<IdentityClaimTag> {
|
||||||
var claims = identityClaims()
|
var claims = this
|
||||||
|
|
||||||
// null leave as is. blank deletes it.
|
// null leave as is. blank deletes it.
|
||||||
if (twitter != null) {
|
if (twitter != null) {
|
||||||
// delete twitter
|
|
||||||
claims = claims.filter { it !is TwitterIdentity }
|
claims = claims.filter { it !is TwitterIdentity }
|
||||||
if (twitter.isNotBlank()) {
|
if (twitter.isNotBlank()) {
|
||||||
TwitterIdentity.parseProofUrl(twitter)?.let { claims = claims + it }
|
TwitterIdentity.parseProofUrl(twitter)?.let { claims = claims + it }
|
||||||
@@ -42,7 +43,6 @@ fun MetadataEvent.replaceClaims(
|
|||||||
|
|
||||||
// null leave as is. blank deletes it.
|
// null leave as is. blank deletes it.
|
||||||
if (github != null) {
|
if (github != null) {
|
||||||
// delete github
|
|
||||||
claims = claims.filter { it !is GitHubIdentity }
|
claims = claims.filter { it !is GitHubIdentity }
|
||||||
if (github.isNotBlank()) {
|
if (github.isNotBlank()) {
|
||||||
GitHubIdentity.parseProofUrl(github)?.let { claims = claims + it }
|
GitHubIdentity.parseProofUrl(github)?.let { claims = claims + it }
|
||||||
@@ -59,3 +59,15 @@ fun MetadataEvent.replaceClaims(
|
|||||||
|
|
||||||
return claims
|
return claims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MetadataEvent.replaceClaims(
|
||||||
|
twitter: String?,
|
||||||
|
mastodon: String?,
|
||||||
|
github: String?,
|
||||||
|
): List<IdentityClaimTag> = identityClaims().replaceClaims(twitter, mastodon, github)
|
||||||
|
|
||||||
|
fun ExternalIdentitiesEvent.replaceClaims(
|
||||||
|
twitter: String?,
|
||||||
|
mastodon: String?,
|
||||||
|
github: String?,
|
||||||
|
): List<IdentityClaimTag> = identityClaims().replaceClaims(twitter, mastodon, github)
|
||||||
|
|||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip39ExtIdentities
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.builder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class ExternalIdentitiesEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
companion object {
|
||||||
|
const val KIND = 10011
|
||||||
|
const val ALT_DESCRIPTION = "External Identities"
|
||||||
|
const val FIXED_D_TAG = ""
|
||||||
|
|
||||||
|
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
|
||||||
|
|
||||||
|
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
|
||||||
|
|
||||||
|
fun createNew(
|
||||||
|
twitter: String? = null,
|
||||||
|
mastodon: String? = null,
|
||||||
|
github: String? = null,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<ExternalIdentitiesEvent>.() -> Unit = {},
|
||||||
|
): EventTemplate<ExternalIdentitiesEvent> =
|
||||||
|
eventTemplate(KIND, "", createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
twitter?.let { twitterClaim(it) }
|
||||||
|
mastodon?.let { mastodonClaim(it) }
|
||||||
|
github?.let { githubClaim(it) }
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFromPast(
|
||||||
|
latest: ExternalIdentitiesEvent,
|
||||||
|
twitter: String? = null,
|
||||||
|
mastodon: String? = null,
|
||||||
|
github: String? = null,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<ExternalIdentitiesEvent>.() -> Unit = {},
|
||||||
|
): EventTemplate<ExternalIdentitiesEvent> {
|
||||||
|
val tags =
|
||||||
|
latest.tags.builder {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
val newClaims = latest.replaceClaims(twitter, mastodon, github)
|
||||||
|
remove(IdentityClaimTag.TAG_NAME)
|
||||||
|
claims(newClaims)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
return EventTemplate(createdAt, KIND, tags, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-8
@@ -20,19 +20,19 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.nip39ExtIdentities
|
package com.vitorpamplona.quartz.nip39ExtIdentities
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
|
||||||
|
|
||||||
fun TagArrayBuilder<MetadataEvent>.claims(identities: List<IdentityClaimTag>) = addAll(identities.map { it.toTagArray() })
|
fun <T : Event> TagArrayBuilder<T>.claims(identities: List<IdentityClaimTag>) = addAll(identities.map { it.toTagArray() })
|
||||||
|
|
||||||
fun TagArrayBuilder<MetadataEvent>.twitterClaim(twitter: TwitterIdentity) = add(twitter.toTagArray())
|
fun <T : Event> TagArrayBuilder<T>.twitterClaim(twitter: TwitterIdentity) = add(twitter.toTagArray())
|
||||||
|
|
||||||
fun TagArrayBuilder<MetadataEvent>.mastodonClaim(mastodon: MastodonIdentity) = add(mastodon.toTagArray())
|
fun <T : Event> TagArrayBuilder<T>.mastodonClaim(mastodon: MastodonIdentity) = add(mastodon.toTagArray())
|
||||||
|
|
||||||
fun TagArrayBuilder<MetadataEvent>.githubClaim(github: GitHubIdentity) = add(github.toTagArray())
|
fun <T : Event> TagArrayBuilder<T>.githubClaim(github: GitHubIdentity) = add(github.toTagArray())
|
||||||
|
|
||||||
fun TagArrayBuilder<MetadataEvent>.twitterClaim(twitterUrl: String) = TwitterIdentity.parseProofUrl(twitterUrl)?.let { twitterClaim(it) }
|
fun <T : Event> TagArrayBuilder<T>.twitterClaim(twitterUrl: String) = TwitterIdentity.parseProofUrl(twitterUrl)?.let { twitterClaim(it) }
|
||||||
|
|
||||||
fun TagArrayBuilder<MetadataEvent>.mastodonClaim(mastodonUrl: String) = MastodonIdentity.parseProofUrl(mastodonUrl)?.let { mastodonClaim(it) }
|
fun <T : Event> TagArrayBuilder<T>.mastodonClaim(mastodonUrl: String) = MastodonIdentity.parseProofUrl(mastodonUrl)?.let { mastodonClaim(it) }
|
||||||
|
|
||||||
fun TagArrayBuilder<MetadataEvent>.githubClaim(githubUrl: String) = GitHubIdentity.parseProofUrl(githubUrl)?.let { githubClaim(it) }
|
fun <T : Event> TagArrayBuilder<T>.githubClaim(githubUrl: String) = GitHubIdentity.parseProofUrl(githubUrl)?.let { githubClaim(it) }
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
|
|||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||||
|
import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent
|
||||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||||
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||||
@@ -224,6 +225,7 @@ class EventFactory {
|
|||||||
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
|
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig)
|
EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
EphemeralChatListEvent.KIND -> EphemeralChatListEvent(id, pubKey, createdAt, tags, content, sig)
|
EphemeralChatListEvent.KIND -> EphemeralChatListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
ExternalIdentitiesEvent.KIND -> ExternalIdentitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
|
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
|
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
Reference in New Issue
Block a user