Adds a Share option with NIP-19 nprofile icon on the User Profile screen

Moves to NProfile instead of NPub to cite users.
Adds Npub or NIP-05 to the QR Screen
This commit is contained in:
Vitor Pamplona
2024-06-21 11:59:58 -04:00
parent 35a9c07636
commit accad0c77a
10 changed files with 281 additions and 201 deletions

View File

@@ -241,33 +241,25 @@ object LocalCache {
return users.get(key) return users.get(key)
} }
fun getAddressableNoteIfExists(key: String): AddressableNote? { fun getAddressableNoteIfExists(key: String): AddressableNote? = addressables.get(key)
return addressables.get(key)
}
fun getNoteIfExists(key: String): Note? { fun getNoteIfExists(key: String): Note? = addressables.get(key) ?: notes.get(key)
return addressables.get(key) ?: notes.get(key)
}
fun getChannelIfExists(key: String): Channel? { fun getChannelIfExists(key: String): Channel? = channels.get(key)
return channels.get(key)
}
fun getNoteIfExists(event: Event): Note? { fun getNoteIfExists(event: Event): Note? =
return if (event is AddressableEvent) { if (event is AddressableEvent) {
getAddressableNoteIfExists(event.addressTag()) getAddressableNoteIfExists(event.addressTag())
} else { } else {
getNoteIfExists(event.id) getNoteIfExists(event.id)
} }
}
fun getOrCreateNote(event: Event): Note { fun getOrCreateNote(event: Event): Note =
return if (event is AddressableEvent) { if (event is AddressableEvent) {
getOrCreateAddressableNote(event.address()) getOrCreateAddressableNote(event.address())
} else { } else {
getOrCreateNote(event.id) getOrCreateNote(event.id)
} }
}
fun checkGetOrCreateNote(key: String): Note? { fun checkGetOrCreateNote(key: String): Note? {
checkNotInMainThread() checkNotInMainThread()
@@ -348,8 +340,8 @@ object LocalCache {
return HexValidator.isHex(key) return HexValidator.isHex(key)
} }
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? { fun checkGetOrCreateAddressableNote(key: String): AddressableNote? =
return try { try {
val addr = ATag.parse(key, null) // relay doesn't matter for the index. val addr = ATag.parse(key, null) // relay doesn't matter for the index.
if (addr != null) { if (addr != null) {
getOrCreateAddressableNote(addr) getOrCreateAddressableNote(addr)
@@ -360,7 +352,6 @@ object LocalCache {
Log.e("LocalCache", "Invalid Key to create channel: $key", e) Log.e("LocalCache", "Invalid Key to create channel: $key", e)
null null
} }
}
fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote { fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote {
// checkNotInMainThread() // checkNotInMainThread()
@@ -395,6 +386,10 @@ object LocalCache {
val newUserMetadata = event.contactMetaData() val newUserMetadata = event.contactMetaData()
if (newUserMetadata != null) { if (newUserMetadata != null) {
oldUser.updateUserInfo(newUserMetadata, event) oldUser.updateUserInfo(newUserMetadata, event)
if (relay != null) {
oldUser.addRelayBeingUsed(relay, event.createdAt)
oldUser.latestMetadataRelay = relay.url
}
} }
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex()} ${oldUser.toBestDisplayName()} from ${relay?.url}") // Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex()} ${oldUser.toBestDisplayName()} from ${relay?.url}")
} else { } else {
@@ -428,11 +423,11 @@ object LocalCache {
} }
} }
fun formattedDateTime(timestamp: Long): String { fun formattedDateTime(timestamp: Long): String =
return Instant.ofEpochSecond(timestamp) Instant
.ofEpochSecond(timestamp)
.atZone(ZoneId.systemDefault()) .atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a")) .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
}
fun consume( fun consume(
event: TextNoteEvent, event: TextNoteEvent,
@@ -755,8 +750,8 @@ object LocalCache {
} }
} }
fun computeReplyTo(event: Event): List<Note> { fun computeReplyTo(event: Event): List<Note> =
return when (event) { when (event) {
is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
@@ -805,7 +800,6 @@ object LocalCache {
else -> emptyList<Note>() else -> emptyList<Note>()
} }
}
fun consume( fun consume(
event: PollNoteEvent, event: PollNoteEvent,
@@ -1218,7 +1212,8 @@ object LocalCache {
if (deletionIndex.add(event)) { if (deletionIndex.add(event)) {
var deletedAtLeastOne = false var deletedAtLeastOne = false
event.deleteEvents() event
.deleteEvents()
.mapNotNull { getNoteIfExists(it) } .mapNotNull { getNoteIfExists(it) }
.forEach { deleteNote -> .forEach { deleteNote ->
// must be the same author // must be the same author
@@ -2030,7 +2025,8 @@ object LocalCache {
suspend fun findStatusesForUser(user: User): ImmutableList<AddressableNote> { suspend fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
checkNotInMainThread() checkNotInMainThread()
return addressables.filter { _, it -> return addressables
.filter { _, it ->
val noteEvent = it.event val noteEvent = it.event
( (
noteEvent is StatusEvent && noteEvent is StatusEvent &&
@@ -2038,8 +2034,7 @@ object LocalCache {
!noteEvent.isExpired() && !noteEvent.isExpired() &&
noteEvent.content.isNotBlank() noteEvent.content.isNotBlank()
) )
} }.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
.reversed() .reversed()
.toImmutableList() .toImmutableList()
} }
@@ -2066,9 +2061,7 @@ object LocalCache {
val modificationCache = LruCache<HexKey, List<Note>>(20) val modificationCache = LruCache<HexKey, List<Note>>(20)
fun cachedModificationEventsForNote(note: Note): List<Note>? { fun cachedModificationEventsForNote(note: Note): List<Note>? = modificationCache[note.idHex]
return modificationCache[note.idHex]
}
suspend fun findLatestModificationForNote(note: Note): List<Note> { suspend fun findLatestModificationForNote(note: Note): List<Note> {
checkNotInMainThread() checkNotInMainThread()
@@ -2082,7 +2075,8 @@ object LocalCache {
val time = TimeUtils.now() val time = TimeUtils.now()
val newNotes = val newNotes =
notes.filter { _, item -> notes
.filter { _, item ->
val noteEvent = item.event val noteEvent = item.event
noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time)
@@ -2210,9 +2204,11 @@ object LocalCache {
note.event is GenericRepostEvent note.event is GenericRepostEvent
) && ) &&
note.replyTo?.any { it.liveSet?.isInUse() == true } != true && note.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
note.liveSet?.isInUse() != true && // don't delete if observing. note.liveSet?.isInUse() != true &&
// don't delete if observing.
note.author?.pubkeyHex !in note.author?.pubkeyHex !in
accounts && // don't delete if it is the logged in account accounts &&
// don't delete if it is the logged in account
note.event?.isTaggedUsers(accounts) != note.event?.isTaggedUsers(accounts) !=
true // don't delete if it's a notification to the logged in user true // don't delete if it's a notification to the logged in user
} }
@@ -2308,8 +2304,7 @@ object LocalCache {
?.hiddenUsers ?.hiddenUsers
?.map { userHex -> ?.map { userHex ->
(notes.filter { _, it -> it.event?.pubKey() == userHex } + addressables.filter { _, it -> it.event?.pubKey() == userHex }).toSet() (notes.filter { _, it -> it.event?.pubKey() == userHex } + addressables.filter { _, it -> it.event?.pubKey() == userHex }).toSet()
} }?.flatten()
?.flatten()
?: emptyList() ?: emptyList()
toBeRemoved.forEach { toBeRemoved.forEach {
@@ -2596,8 +2591,8 @@ object LocalCache {
} }
} }
fun hasConsumed(notificationEvent: Event): Boolean { fun hasConsumed(notificationEvent: Event): Boolean =
return if (notificationEvent is AddressableEvent) { if (notificationEvent is AddressableEvent) {
val note = addressables.get(notificationEvent.addressTag()) val note = addressables.get(notificationEvent.addressTag())
val noteEvent = note?.event val noteEvent = note?.event
noteEvent != null && notificationEvent.createdAt <= noteEvent.createdAt() noteEvent != null && notificationEvent.createdAt <= noteEvent.createdAt()
@@ -2606,7 +2601,6 @@ object LocalCache {
note?.event != null note?.event != null
} }
} }
}
@Stable @Stable
class LocalCacheLiveData { class LocalCacheLiveData {
@@ -2617,8 +2611,7 @@ class LocalCacheLiveData {
private val bundler = BundledInsert<Note>(1000, Dispatchers.IO) private val bundler = BundledInsert<Note>(1000, Dispatchers.IO)
fun invalidateData(newNote: Note) { fun invalidateData(newNote: Note) {
bundler.invalidateList(newNote) { bundler.invalidateList(newNote) { bundledNewNotes ->
bundledNewNotes ->
_newEventBundles.emit(bundledNewNotes) _newEventBundles.emit(bundledNewNotes)
} }
} }

View File

@@ -34,7 +34,9 @@ import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Lud06 import com.vitorpamplona.quartz.encoders.Lud06
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.ContactListEvent
@@ -49,10 +51,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import java.math.BigDecimal import java.math.BigDecimal
@Stable @Stable
class User(val pubkeyHex: String) { class User(
val pubkeyHex: String,
) {
var info: UserMetadata? = null var info: UserMetadata? = null
var latestMetadata: MetadataEvent? = null var latestMetadata: MetadataEvent? = null
var latestMetadataRelay: String? = null
var latestContactList: ContactListEvent? = null var latestContactList: ContactListEvent? = null
var latestBookmarkList: BookmarkListEvent? = null var latestBookmarkList: BookmarkListEvent? = null
@@ -76,7 +81,16 @@ class User(val pubkeyHex: String) {
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex() fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
fun toNostrUri() = "nostr:${pubkeyNpub()}" fun toNProfile(): String {
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)?.writeRelays()
return Nip19Bech32.createNProfile(
pubkeyHex,
relayList?.take(3) ?: listOfNotNull(latestMetadataRelay),
)
}
fun toNostrUri() = "nostr:${toNProfile()}"
override fun toString(): String = pubkeyHex override fun toString(): String = pubkeyHex
@@ -96,17 +110,11 @@ class User(val pubkeyHex: String) {
return firstName return firstName
} }
fun toBestDisplayName(): String { fun toBestDisplayName(): String = info?.bestName() ?: pubkeyDisplayHex()
return info?.bestName() ?: pubkeyDisplayHex()
}
fun nip05(): String? { fun nip05(): String? = info?.nip05
return info?.nip05
}
fun profilePicture(): String? { fun profilePicture(): String? = info?.picture
return info?.picture
}
fun updateBookmark(event: BookmarkListEvent) { fun updateBookmark(event: BookmarkListEvent) {
if (event.id == latestBookmarkList?.id) return if (event.id == latestBookmarkList?.id) return
@@ -132,10 +140,18 @@ class User(val pubkeyHex: String) {
// Update Followers of the past user list // Update Followers of the past user list
// Update Followers of the new contact list // Update Followers of the new contact list
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData() LocalCache
.getUserIfExists(it)
?.liveSet
?.innerFollowers
?.invalidateData()
} }
(latestContactList)?.unverifiedFollowKeySet()?.forEach { (latestContactList)?.unverifiedFollowKeySet()?.forEach {
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData() LocalCache
.getUserIfExists(it)
?.liveSet
?.innerFollowers
?.invalidateData()
} }
liveSet?.innerRelays?.invalidateData() liveSet?.innerRelays?.invalidateData()
@@ -198,25 +214,19 @@ class User(val pubkeyHex: String) {
return amount return amount
} }
fun reportsBy(user: User): Set<Note> { fun reportsBy(user: User): Set<Note> = reports[user] ?: emptySet()
return reports[user] ?: emptySet()
}
fun countReportAuthorsBy(users: Set<HexKey>): Int { fun countReportAuthorsBy(users: Set<HexKey>): Int = reports.count { it.key.pubkeyHex in users }
return reports.count { it.key.pubkeyHex in users }
}
fun reportsBy(users: Set<HexKey>): List<Note> { fun reportsBy(users: Set<HexKey>): List<Note> =
return reports reports
.mapNotNull { .mapNotNull {
if (it.key.pubkeyHex in users) { if (it.key.pubkeyHex in users) {
it.value it.value
} else { } else {
null null
} }
} }.flatten()
.flatten()
}
@Synchronized @Synchronized
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom { private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom {
@@ -235,9 +245,7 @@ class User(val pubkeyHex: String) {
return getOrCreatePrivateChatroom(key) return getOrCreatePrivateChatroom(key)
} }
private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom { private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom = privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key)
return privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key)
}
fun addMessage( fun addMessage(
room: ChatroomKey, room: ChatroomKey,
@@ -326,13 +334,9 @@ class User(val pubkeyHex: String) {
liveSet?.innerMetadata?.invalidateData() liveSet?.innerMetadata?.invalidateData()
} }
fun isFollowing(user: User): Boolean { fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
return latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
}
fun isFollowingHashtag(tag: String): Boolean { fun isFollowingHashtag(tag: String): Boolean = latestContactList?.isTaggedHash(tag) ?: false
return latestContactList?.isTaggedHash(tag) ?: false
}
fun isFollowingHashtagCached(tag: String): Boolean { fun isFollowingHashtagCached(tag: String): Boolean {
return latestContactList?.verifiedFollowTagSet?.let { return latestContactList?.verifiedFollowTagSet?.let {
@@ -362,37 +366,21 @@ class User(val pubkeyHex: String) {
?: false ?: false
} }
fun transientFollowCount(): Int? { fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size
return latestContactList?.unverifiedFollowKeySet()?.size
}
suspend fun transientFollowerCount(): Int { suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
return LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
}
fun cachedFollowingKeySet(): Set<HexKey> { fun cachedFollowingKeySet(): Set<HexKey> = latestContactList?.verifiedFollowKeySet ?: emptySet()
return latestContactList?.verifiedFollowKeySet ?: emptySet()
}
fun cachedFollowingTagSet(): Set<String> { fun cachedFollowingTagSet(): Set<String> = latestContactList?.verifiedFollowTagSet ?: emptySet()
return latestContactList?.verifiedFollowTagSet ?: emptySet()
}
fun cachedFollowingGeohashSet(): Set<HexKey> { fun cachedFollowingGeohashSet(): Set<HexKey> = latestContactList?.verifiedFollowGeohashSet ?: emptySet()
return latestContactList?.verifiedFollowGeohashSet ?: emptySet()
}
fun cachedFollowingCommunitiesSet(): Set<HexKey> { fun cachedFollowingCommunitiesSet(): Set<HexKey> = latestContactList?.verifiedFollowCommunitySet ?: emptySet()
return latestContactList?.verifiedFollowCommunitySet ?: emptySet()
}
fun cachedFollowCount(): Int? { fun cachedFollowCount(): Int? = latestContactList?.verifiedFollowKeySet?.size
return latestContactList?.verifiedFollowKeySet?.size
}
suspend fun cachedFollowerCount(): Int { suspend fun cachedFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
return LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
}
fun hasSentMessagesTo(key: ChatroomKey?): Boolean { fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
val messagesToUser = privateChatrooms[key] ?: return false val messagesToUser = privateChatrooms[key] ?: return false
@@ -403,16 +391,13 @@ class User(val pubkeyHex: String) {
fun hasReport( fun hasReport(
loggedIn: User, loggedIn: User,
type: ReportEvent.ReportType, type: ReportEvent.ReportType,
): Boolean { ): Boolean =
return reports[loggedIn]?.firstOrNull { reports[loggedIn]?.firstOrNull {
it.event is ReportEvent && it.event is ReportEvent &&
(it.event as ReportEvent).reportedAuthor().any { it.reportType == type } (it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
} != null } != null
}
fun anyNameStartsWith(username: String): Boolean { fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
return info?.anyNameStartsWith(username) ?: false
}
var liveSet: UserLiveSet? = null var liveSet: UserLiveSet? = null
var flowSet: UserFlowSet? = null var flowSet: UserFlowSet? = null
@@ -473,14 +458,14 @@ class User(val pubkeyHex: String) {
} }
@Stable @Stable
class UserFlowSet(u: User) { class UserFlowSet(
u: User,
) {
// Observers line up here. // Observers line up here.
val follows = UserBundledRefresherFlow(u) val follows = UserBundledRefresherFlow(u)
val relays = UserBundledRefresherFlow(u) val relays = UserBundledRefresherFlow(u)
fun isInUse(): Boolean { fun isInUse(): Boolean = relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
return relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
}
fun destroy() { fun destroy() {
relays.destroy() relays.destroy()
@@ -489,7 +474,9 @@ class UserFlowSet(u: User) {
} }
@Stable @Stable
class UserLiveSet(u: User) { class UserLiveSet(
u: User,
) {
val innerMetadata = UserBundledRefresherLiveData(u) val innerMetadata = UserBundledRefresherLiveData(u)
// UI Observers line up here. // UI Observers line up here.
@@ -521,8 +508,8 @@ class UserLiveSet(u: User) {
val userMetadataInfo = innerMetadata.map { it.user.info }.distinctUntilChanged() val userMetadataInfo = innerMetadata.map { it.user.info }.distinctUntilChanged()
fun isInUse(): Boolean { fun isInUse(): Boolean =
return metadata.hasObservers() || metadata.hasObservers() ||
follows.hasObservers() || follows.hasObservers() ||
followers.hasObservers() || followers.hasObservers() ||
reports.hasObservers() || reports.hasObservers() ||
@@ -535,7 +522,6 @@ class UserLiveSet(u: User) {
profilePictureChanges.hasObservers() || profilePictureChanges.hasObservers() ||
nip05Changes.hasObservers() || nip05Changes.hasObservers() ||
userMetadataInfo.hasObservers() userMetadataInfo.hasObservers()
}
fun destroy() { fun destroy() {
innerMetadata.destroy() innerMetadata.destroy()
@@ -558,7 +544,9 @@ data class RelayInfo(
var counter: Long, var counter: Long,
) )
class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserState(user)) { class UserBundledRefresherLiveData(
val user: User,
) : LiveData<UserState>(UserState(user)) {
// Refreshes observers in batches. // Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO) private val bundler = BundledUpdate(500, Dispatchers.IO)
@@ -585,7 +573,9 @@ class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserSta
} }
@Stable @Stable
class UserBundledRefresherFlow(val user: User) { class UserBundledRefresherFlow(
val user: User,
) {
// Refreshes observers in batches. // Refreshes observers in batches.
private val bundler = BundledUpdate(500, Dispatchers.IO) private val bundler = BundledUpdate(500, Dispatchers.IO)
val stateFlow = MutableStateFlow(UserState(user)) val stateFlow = MutableStateFlow(UserState(user))
@@ -605,7 +595,10 @@ class UserBundledRefresherFlow(val user: User) {
} }
} }
class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveData<Y>(initialValue) { class UserLoadingLiveData<Y>(
val user: User,
initialValue: Y?,
) : MediatorLiveData<Y>(initialValue) {
override fun onActive() { override fun onActive() {
super.onActive() super.onActive()
NostrSingleUserDataSource.add(user) NostrSingleUserDataSource.add(user)
@@ -617,4 +610,6 @@ class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveDat
} }
} }
@Immutable class UserState(val user: User) @Immutable class UserState(
val user: User,
)

View File

@@ -100,10 +100,10 @@ class NewMessageTagger(
val results = parseDirtyWordForKey(word) val results = parseDirtyWordForKey(word)
when (val entity = results?.key?.entity) { when (val entity = results?.key?.entity) {
is Nip19Bech32.NPub -> { is Nip19Bech32.NPub -> {
getNostrAddress(dao.getOrCreateUser(entity.hex).pubkeyNpub(), results.restOfWord) getNostrAddress(dao.getOrCreateUser(entity.hex).toNProfile(), results.restOfWord)
} }
is Nip19Bech32.NProfile -> { is Nip19Bech32.NProfile -> {
getNostrAddress(dao.getOrCreateUser(entity.hex).pubkeyNpub(), results.restOfWord) getNostrAddress(dao.getOrCreateUser(entity.hex).toNProfile(), results.restOfWord)
} }
is Nip19Bech32.Note -> { is Nip19Bech32.Note -> {
@@ -138,17 +138,15 @@ class NewMessageTagger(
word word
} }
} }
} }.joinToString(" ")
.joinToString(" ") }.joinToString("\n")
}
.joinToString("\n")
} }
fun getNostrAddress( fun getNostrAddress(
bechAddress: String, bechAddress: String,
restOfTheWord: String?, restOfTheWord: String?,
): String { ): String =
return if (restOfTheWord.isNullOrEmpty()) { if (restOfTheWord.isNullOrEmpty()) {
"nostr:$bechAddress" "nostr:$bechAddress"
} else { } else {
if (Bech32.ALPHABET.contains(restOfTheWord.get(0), true)) { if (Bech32.ALPHABET.contains(restOfTheWord.get(0), true)) {
@@ -157,9 +155,11 @@ class NewMessageTagger(
"nostr:${bechAddress}$restOfTheWord" "nostr:${bechAddress}$restOfTheWord"
} }
} }
}
@Immutable data class DirtyKeyInfo(val key: Nip19Bech32.ParseReturn, val restOfWord: String?) @Immutable data class DirtyKeyInfo(
val key: Nip19Bech32.ParseReturn,
val restOfWord: String?,
)
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? { fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
var key = mightBeAKey var key = mightBeAKey

View File

@@ -168,8 +168,7 @@ fun DrawerContent(
BottomContent( BottomContent(
accountViewModel.account.userProfile(), accountViewModel.account.userProfile(),
drawerState, drawerState,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value, accountViewModel,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
nav, nav,
) )
} }
@@ -741,8 +740,7 @@ fun IconRowRelays(
fun BottomContent( fun BottomContent(
user: User, user: User,
drawerState: DrawerState, drawerState: DrawerState,
loadProfilePicture: Boolean, accountViewModel: AccountViewModel,
loadRobohash: Boolean,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -800,8 +798,7 @@ fun BottomContent(
if (dialogOpen) { if (dialogOpen) {
ShowQRDialog( ShowQRDialog(
user, user,
loadProfilePicture = loadProfilePicture, accountViewModel,
loadRobohash = loadRobohash,
onScan = { onScan = {
dialogOpen = false dialogOpen = false
coroutineScope.launch { drawerState.close() } coroutineScope.launch { drawerState.close() }

View File

@@ -310,7 +310,7 @@ fun DisplayStatus(
} }
@Composable @Composable
private fun DisplayNIP05( fun DisplayNIP05(
nip05: String, nip05: String,
nip05Verified: MutableState<Boolean?>, nip05Verified: MutableState<Boolean?>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,

View File

@@ -84,6 +84,7 @@ import androidx.core.graphics.ColorUtils
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -95,7 +96,6 @@ import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
import com.vitorpamplona.quartz.events.AudioTrackEvent import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PeopleListEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private fun lightenColor( private fun lightenColor(
@@ -110,6 +110,10 @@ private fun lightenColor(
return Color(argb) return Color(argb)
} }
val externalLinkForUser = { user: User ->
"https://njump.me/${user.toNProfile()}"
}
val externalLinkForNote = { note: Note -> val externalLinkForNote = { note: Note ->
if (note is AddressableNote) { if (note is AddressableNote) {
if (note.event?.getReward() != null) { if (note.event?.getReward() != null) {
@@ -298,19 +302,21 @@ private fun RenderMainPopup(
Icons.Default.AlternateEmail, Icons.Default.AlternateEmail,
stringRes(R.string.quick_action_copy_user_id), stringRes(R.string.quick_action_copy_user_id),
) { ) {
scope.launch(Dispatchers.IO) { note.author?.let {
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}")) scope.launch {
clipboardManager.setText(AnnotatedString(it.toNostrUri()))
showToast(R.string.copied_user_id_to_clipboard) showToast(R.string.copied_user_id_to_clipboard)
onDismiss() onDismiss()
} }
} }
}
VerticalDivider(color = primaryLight) VerticalDivider(color = primaryLight)
NoteQuickActionItem( NoteQuickActionItem(
Icons.Default.FormatQuote, Icons.Default.FormatQuote,
stringRes(R.string.quick_action_copy_note_id), stringRes(R.string.quick_action_copy_note_id),
) { ) {
scope.launch(Dispatchers.IO) { scope.launch {
clipboardManager.setText(AnnotatedString("nostr:${note.toNEvent()}")) clipboardManager.setText(AnnotatedString(note.toNostrUri()))
showToast(R.string.copied_note_id_to_clipboard) showToast(R.string.copied_note_id_to_clipboard)
onDismiss() onDismiss()
} }

View File

@@ -197,17 +197,19 @@ fun NoteDropDownMenu(
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringRes(R.string.copy_user_pubkey)) }, text = { Text(stringRes(R.string.copy_user_pubkey)) },
onClick = { onClick = {
note.author?.let {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}")) clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}"))
onDismiss() onDismiss()
} }
}
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringRes(R.string.copy_note_id)) }, text = { Text(stringRes(R.string.copy_note_id)) },
onClick = { onClick = {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent())) clipboardManager.setText(AnnotatedString(note.toNostrUri()))
onDismiss() onDismiss()
} }
}, },

View File

@@ -45,16 +45,22 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji 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.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.UserMetadata
@@ -62,21 +68,20 @@ import com.vitorpamplona.quartz.events.UserMetadata
@Preview @Preview
@Composable @Composable
fun ShowQRDialogPreview() { fun ShowQRDialogPreview() {
val user = User("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") val accountViewModel = mockAccountViewModel()
accountViewModel.userProfile().info =
user.info =
UserMetadata().apply { UserMetadata().apply {
name = "My Name" name = "My Name"
picture = "Picture" picture = "Picture"
nip05 = null
banner = "http://banner.com/test" banner = "http://banner.com/test"
website = "http://mywebsite.com/test" website = "http://mywebsite.com/test"
about = "This is the about me" about = "This is the about me"
} }
ShowQRDialog( ShowQRDialog(
user = user, user = accountViewModel.userProfile(),
loadProfilePicture = false, accountViewModel = accountViewModel,
loadRobohash = false,
onScan = {}, onScan = {},
onClose = {}, onClose = {},
) )
@@ -85,8 +90,7 @@ fun ShowQRDialogPreview() {
@Composable @Composable
fun ShowQRDialog( fun ShowQRDialog(
user: User, user: User,
loadProfilePicture: Boolean, accountViewModel: AccountViewModel,
loadRobohash: Boolean,
onScan: (String) -> Unit, onScan: (String) -> Unit,
onClose: () -> Unit, onClose: () -> Unit,
) { ) {
@@ -126,8 +130,8 @@ fun ShowQRDialog(
.height(100.dp) .height(100.dp)
.clip(shape = CircleShape) .clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape), .border(3.dp, MaterialTheme.colorScheme.background, CircleShape),
loadProfilePicture = loadProfilePicture, loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = loadRobohash, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
) )
} }
Row( Row(
@@ -141,13 +145,34 @@ fun ShowQRDialog(
fontSize = 18.sp, fontSize = 18.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 = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
} }
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = Size35dp), modifier = Modifier.fillMaxWidth().padding(horizontal = Size35dp),
) { ) {
QrCodeDrawer("nostr:${user.pubkeyNpub()}") QrCodeDrawer(user.toNostrUri())
} }
Row(modifier = Modifier.padding(horizontal = 30.dp)) { Row(modifier = Modifier.padding(horizontal = 30.dp)) {

View File

@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Intent
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@@ -103,6 +104,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -136,6 +138,7 @@ import com.vitorpamplona.amethyst.ui.note.DrawPlayName
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.externalLinkForUser
import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.qrcode.ShowQRDialog import com.vitorpamplona.amethyst.ui.qrcode.ShowQRDialog
import com.vitorpamplona.amethyst.ui.screen.FeedState import com.vitorpamplona.amethyst.ui.screen.FeedState
@@ -996,9 +999,8 @@ private fun DrawAdditionalInfo(
if (dialogOpen) { if (dialogOpen) {
ShowQRDialog( ShowQRDialog(
user, user = user,
accountViewModel.settings.showProfilePictures.value, accountViewModel = accountViewModel,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
onScan = { onScan = {
dialogOpen = false dialogOpen = false
nav(it) nav(it)
@@ -1870,6 +1872,32 @@ fun UserProfileDropDownMenu(
}, },
) )
val actContext = LocalContext.current
DropdownMenuItem(
text = { Text(stringRes(R.string.quick_action_share)) },
onClick = {
val sendIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(
Intent.EXTRA_TEXT,
externalLinkForUser(user),
)
putExtra(
Intent.EXTRA_TITLE,
stringRes(actContext, R.string.quick_action_share_browser_link),
)
}
val shareIntent =
Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share))
ContextCompat.startActivity(actContext, shareIntent, null)
onDismiss()
},
)
if (accountViewModel.userProfile() != user) { if (accountViewModel.userProfile() != user) {
HorizontalDivider(thickness = DividerThickness) HorizontalDivider(thickness = DividerThickness)
if (accountViewModel.account.isHidden(user)) { if (accountViewModel.account.isHidden(user)) {

View File

@@ -39,7 +39,9 @@ object Nip19Bech32 {
ADDRESS, ADDRESS,
} }
enum class TlvTypes(val id: Byte) { enum class TlvTypes(
val id: Byte,
) {
SPECIAL(0), SPECIAL(0),
RELAY(1), RELAY(1),
AUTHOR(2), AUTHOR(2),
@@ -53,33 +55,59 @@ object Nip19Bech32 {
) )
@Immutable @Immutable
data class ParseReturn(val entity: Entity, val additionalChars: String? = null) data class ParseReturn(
val entity: Entity,
val additionalChars: String? = null,
)
interface Entity interface Entity
@Immutable @Immutable
data class NSec(val hex: String) : Entity data class NSec(
val hex: String,
) : Entity
@Immutable @Immutable
data class NPub(val hex: String) : Entity data class NPub(
val hex: String,
) : Entity
@Immutable @Immutable
data class Note(val hex: String) : Entity data class Note(
val hex: String,
) : Entity
@Immutable @Immutable
data class NProfile(val hex: String, val relay: List<String>) : Entity data class NProfile(
val hex: String,
val relay: List<String>,
) : Entity
@Immutable @Immutable
data class NEvent(val hex: String, val relay: List<String>, val author: String?, val kind: Int?) : Entity data class NEvent(
val hex: String,
val relay: List<String>,
val author: String?,
val kind: Int?,
) : Entity
@Immutable @Immutable
data class NAddress(val atag: String, val relay: List<String>, val author: String, val kind: Int) : Entity data class NAddress(
val atag: String,
val relay: List<String>,
val author: String,
val kind: Int,
) : Entity
@Immutable @Immutable
data class NRelay(val relay: List<String>) : Entity data class NRelay(
val relay: List<String>,
) : Entity
@Immutable @Immutable
data class NEmbed(val event: Event) : Entity data class NEmbed(
val event: Event,
) : Entity
fun uriToRoute(uri: String?): ParseReturn? { fun uriToRoute(uri: String?): ParseReturn? {
if (uri == null) return null if (uri == null) return null
@@ -108,8 +136,8 @@ object Nip19Bech32 {
type: String, type: String,
key: String?, key: String?,
additionalChars: String?, additionalChars: String?,
): ParseReturn? { ): ParseReturn? =
return try { try {
val bytes = (type + key).bechToBytes() val bytes = (type + key).bechToBytes()
when (type.lowercase()) { when (type.lowercase()) {
@@ -129,7 +157,6 @@ object Nip19Bech32 {
Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e) Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e)
null null
} }
}
private fun nembed(bytes: ByteArray): NEmbed? { private fun nembed(bytes: ByteArray): NEmbed? {
if (bytes.isEmpty()) return null if (bytes.isEmpty()) return null
@@ -205,21 +232,30 @@ object Nip19Bech32 {
author: String?, author: String?,
kind: Int?, kind: Int?,
relay: String?, relay: String?,
): String { ): String =
return TlvBuilder() TlvBuilder()
.apply { .apply {
addHex(TlvTypes.SPECIAL, idHex) addHex(TlvTypes.SPECIAL, idHex)
addStringIfNotNull(TlvTypes.RELAY, relay) addStringIfNotNull(TlvTypes.RELAY, relay)
addHexIfNotNull(TlvTypes.AUTHOR, author) addHexIfNotNull(TlvTypes.AUTHOR, author)
addIntIfNotNull(TlvTypes.KIND, kind) addIntIfNotNull(TlvTypes.KIND, kind)
} }.build()
.build()
.toNEvent() .toNEvent()
}
fun createNEmbed(event: Event): String { fun createNProfile(
return gzip(event.toJson()).toNEmbed() authorPubKeyHex: String,
relay: List<String>,
): String =
TlvBuilder()
.apply {
addHex(TlvTypes.SPECIAL, authorPubKeyHex)
relay.forEach {
addStringIfNotNull(TlvTypes.RELAY, it)
} }
}.build()
.toNProfile()
fun createNEmbed(event: Event): String = gzip(event.toJson()).toNEmbed()
fun gzip(content: String): ByteArray { fun gzip(content: String): ByteArray {
val bos = ByteArrayOutputStream() val bos = ByteArrayOutputStream()
@@ -239,23 +275,24 @@ fun ByteArray.toNote() = Bech32.encodeBytes(hrp = "note", this, Bech32.Encoding.
fun ByteArray.toNEvent() = Bech32.encodeBytes(hrp = "nevent", this, Bech32.Encoding.Bech32) fun ByteArray.toNEvent() = Bech32.encodeBytes(hrp = "nevent", this, Bech32.Encoding.Bech32)
fun ByteArray.toNProfile() = Bech32.encodeBytes(hrp = "nprofile", this, Bech32.Encoding.Bech32)
fun ByteArray.toNAddress() = Bech32.encodeBytes(hrp = "naddr", this, Bech32.Encoding.Bech32) fun ByteArray.toNAddress() = Bech32.encodeBytes(hrp = "naddr", this, Bech32.Encoding.Bech32)
fun ByteArray.toLnUrl() = Bech32.encodeBytes(hrp = "lnurl", this, Bech32.Encoding.Bech32) fun ByteArray.toLnUrl() = Bech32.encodeBytes(hrp = "lnurl", this, Bech32.Encoding.Bech32)
fun ByteArray.toNEmbed() = Bech32.encodeBytes(hrp = "nembed", this, Bech32.Encoding.Bech32) fun ByteArray.toNEmbed() = Bech32.encodeBytes(hrp = "nembed", this, Bech32.Encoding.Bech32)
fun decodePublicKey(key: String): ByteArray { fun decodePublicKey(key: String): ByteArray =
return when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) {
is Nip19Bech32.NSec -> KeyPair(privKey = key.bechToBytes()).pubKey is Nip19Bech32.NSec -> KeyPair(privKey = key.bechToBytes()).pubKey
is Nip19Bech32.NPub -> parsed.hex.hexToByteArray() is Nip19Bech32.NPub -> parsed.hex.hexToByteArray()
is Nip19Bech32.NProfile -> parsed.hex.hexToByteArray() is Nip19Bech32.NProfile -> parsed.hex.hexToByteArray()
else -> Hex.decode(key) // crashes on purpose else -> Hex.decode(key) // crashes on purpose
} }
}
fun decodePrivateKeyAsHexOrNull(key: String): HexKey? { fun decodePrivateKeyAsHexOrNull(key: String): HexKey? =
return try { try {
when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) {
is Nip19Bech32.NSec -> parsed.hex is Nip19Bech32.NSec -> parsed.hex
is Nip19Bech32.NPub -> null is Nip19Bech32.NPub -> null
@@ -271,10 +308,9 @@ fun decodePrivateKeyAsHexOrNull(key: String): HexKey? {
if (e is CancellationException) throw e if (e is CancellationException) throw e
null null
} }
}
fun decodePublicKeyAsHexOrNull(key: String): HexKey? { fun decodePublicKeyAsHexOrNull(key: String): HexKey? =
return try { try {
when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) {
is Nip19Bech32.NSec -> KeyPair(privKey = key.bechToBytes()).pubKey.toHexKey() is Nip19Bech32.NSec -> KeyPair(privKey = key.bechToBytes()).pubKey.toHexKey()
is Nip19Bech32.NPub -> parsed.hex is Nip19Bech32.NPub -> parsed.hex
@@ -290,10 +326,9 @@ fun decodePublicKeyAsHexOrNull(key: String): HexKey? {
if (e is CancellationException) throw e if (e is CancellationException) throw e
null null
} }
}
fun decodeEventIdAsHexOrNull(key: String): HexKey? { fun decodeEventIdAsHexOrNull(key: String): HexKey? =
return try { try {
when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) {
is Nip19Bech32.NSec -> null is Nip19Bech32.NSec -> null
is Nip19Bech32.NPub -> null is Nip19Bech32.NPub -> null
@@ -309,7 +344,6 @@ fun decodeEventIdAsHexOrNull(key: String): HexKey? {
if (e is CancellationException) throw e if (e is CancellationException) throw e
null null
} }
}
fun TlvBuilder.addString( fun TlvBuilder.addString(
type: Nip19Bech32.TlvTypes, type: Nip19Bech32.TlvTypes,