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
pull/938/head
Vitor Pamplona 2024-06-21 11:59:58 -04:00
rodzic 35a9c07636
commit accad0c77a
10 zmienionych plików z 281 dodań i 201 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -45,16 +45,22 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.CloseButton
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.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.theme.Size35dp
import com.vitorpamplona.quartz.events.UserMetadata
@ -62,21 +68,20 @@ import com.vitorpamplona.quartz.events.UserMetadata
@Preview
@Composable
fun ShowQRDialogPreview() {
val user = User("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
user.info =
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"
}
ShowQRDialog(
user = user,
loadProfilePicture = false,
loadRobohash = false,
user = accountViewModel.userProfile(),
accountViewModel = accountViewModel,
onScan = {},
onClose = {},
)
@ -85,8 +90,7 @@ fun ShowQRDialogPreview() {
@Composable
fun ShowQRDialog(
user: User,
loadProfilePicture: Boolean,
loadRobohash: Boolean,
accountViewModel: AccountViewModel,
onScan: (String) -> Unit,
onClose: () -> Unit,
) {
@ -126,8 +130,8 @@ fun ShowQRDialog(
.height(100.dp)
.clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape),
loadProfilePicture = loadProfilePicture,
loadRobohash = loadRobohash,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
Row(
@ -141,13 +145,34 @@ fun ShowQRDialog(
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(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = Size35dp),
) {
QrCodeDrawer("nostr:${user.pubkeyNpub()}")
QrCodeDrawer(user.toNostrUri())
}
Row(modifier = Modifier.padding(horizontal = 30.dp)) {

Wyświetl plik

@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Intent
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
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.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
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.LightningAddressIcon
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.qrcode.ShowQRDialog
import com.vitorpamplona.amethyst.ui.screen.FeedState
@ -996,9 +999,8 @@ private fun DrawAdditionalInfo(
if (dialogOpen) {
ShowQRDialog(
user,
accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
user = user,
accountViewModel = accountViewModel,
onScan = {
dialogOpen = false
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) {
HorizontalDivider(thickness = DividerThickness)
if (accountViewModel.account.isHidden(user)) {

Wyświetl plik

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