mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-10 02:42:34 +02:00
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:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
)
|
||||||
|
@@ -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
|
||||||
|
@@ -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() }
|
||||||
|
@@ -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,
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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)) {
|
||||||
|
@@ -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)) {
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user