Merge remote-tracking branch 'origin/HEAD' into new_wallet_connect
This commit is contained in:
+1
-1
@@ -137,7 +137,7 @@ dependencies {
|
|||||||
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha05'
|
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha05'
|
||||||
|
|
||||||
// view videos
|
// view videos
|
||||||
implementation 'com.google.android.exoplayer:exoplayer:2.18.5'
|
implementation 'com.google.android.exoplayer:exoplayer:2.18.6'
|
||||||
|
|
||||||
// Load images from the web.
|
// Load images from the web.
|
||||||
implementation "io.coil-kt:coil-compose:$coil_version"
|
implementation "io.coil-kt:coil-compose:$coil_version"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.model.toByteArray
|
|||||||
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
|
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.Nip47URI
|
import com.vitorpamplona.amethyst.ui.note.Nip47URI
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
import nostr.postr.Persona
|
import nostr.postr.Persona
|
||||||
@@ -42,6 +43,7 @@ private object PrefKeys {
|
|||||||
const val LANGUAGE_PREFS = "languagePreferences"
|
const val LANGUAGE_PREFS = "languagePreferences"
|
||||||
const val TRANSLATE_TO = "translateTo"
|
const val TRANSLATE_TO = "translateTo"
|
||||||
const val ZAP_AMOUNTS = "zapAmounts"
|
const val ZAP_AMOUNTS = "zapAmounts"
|
||||||
|
const val DEFAULT_ZAPTYPE = "defaultZapType"
|
||||||
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
||||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||||
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
||||||
@@ -191,6 +193,7 @@ object LocalPreferences {
|
|||||||
putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(account.languagePreferences))
|
putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(account.languagePreferences))
|
||||||
putString(PrefKeys.TRANSLATE_TO, account.translateTo)
|
putString(PrefKeys.TRANSLATE_TO, account.translateTo)
|
||||||
putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
|
putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
|
||||||
|
putString(PrefKeys.DEFAULT_ZAPTYPE, gson.toJson(account.defaultZapType))
|
||||||
putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest))
|
putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest))
|
||||||
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
||||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
||||||
@@ -217,6 +220,11 @@ object LocalPreferences {
|
|||||||
object : TypeToken<List<Long>>() {}.type
|
object : TypeToken<List<Long>>() {}.type
|
||||||
) ?: listOf(500L, 1000L, 5000L)
|
) ?: listOf(500L, 1000L, 5000L)
|
||||||
|
|
||||||
|
val defaultZapType = gson.fromJson(
|
||||||
|
getString(PrefKeys.DEFAULT_ZAPTYPE, "PUBLIC"),
|
||||||
|
object : TypeToken<LnZapEvent.ZapType>() {}.type
|
||||||
|
) ?: LnZapEvent.ZapType.PUBLIC
|
||||||
|
|
||||||
val zapPaymentRequestServer = try {
|
val zapPaymentRequestServer = try {
|
||||||
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
||||||
gson.fromJson(it, Nip47URI::class.java)
|
gson.fromJson(it, Nip47URI::class.java)
|
||||||
@@ -260,6 +268,7 @@ object LocalPreferences {
|
|||||||
languagePreferences,
|
languagePreferences,
|
||||||
translateTo,
|
translateTo,
|
||||||
zapAmountChoices,
|
zapAmountChoices,
|
||||||
|
defaultZapType,
|
||||||
zapPaymentRequestServer,
|
zapPaymentRequestServer,
|
||||||
hideDeleteRequestDialog,
|
hideDeleteRequestDialog,
|
||||||
hideBlockAlertDialog,
|
hideBlockAlertDialog,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class Account(
|
|||||||
var languagePreferences: Map<String, String> = mapOf(),
|
var languagePreferences: Map<String, String> = mapOf(),
|
||||||
var translateTo: String = Locale.getDefault().language,
|
var translateTo: String = Locale.getDefault().language,
|
||||||
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
||||||
|
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC,
|
||||||
var zapPaymentRequest: Nip47URI? = null,
|
var zapPaymentRequest: Nip47URI? = null,
|
||||||
var hideDeleteRequestDialog: Boolean = false,
|
var hideDeleteRequestDialog: Boolean = false,
|
||||||
var hideBlockAlertDialog: Boolean = false,
|
var hideBlockAlertDialog: Boolean = false,
|
||||||
@@ -632,6 +633,12 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeDefaultZapType(zapType: LnZapEvent.ZapType) {
|
||||||
|
defaultZapType = zapType
|
||||||
|
live.invalidateData()
|
||||||
|
saveable.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
fun changeZapAmounts(newAmounts: List<Long>) {
|
fun changeZapAmounts(newAmounts: List<Long>) {
|
||||||
zapAmountChoices = newAmounts
|
zapAmountChoices = newAmounts
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
|
|||||||
@@ -84,15 +84,15 @@ fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
|
|||||||
} else if (key.startsWith("nprofile", true)) {
|
} else if (key.startsWith("nprofile", true)) {
|
||||||
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
||||||
|
|
||||||
return DirtyKeyInfo(pubkeyRelay, restOfWord)
|
return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
|
||||||
} else if (key.startsWith("nevent", true)) {
|
} else if (key.startsWith("nevent1", true)) {
|
||||||
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
||||||
|
|
||||||
return DirtyKeyInfo(noteRelayId, restOfWord)
|
return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
|
||||||
} else if (key.startsWith("naddr1", true)) {
|
} else if (key.startsWith("naddr1", true)) {
|
||||||
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
||||||
|
|
||||||
return DirtyKeyInfo(address, "") // no way to know when they address ends and dirt begins
|
return DirtyKeyInfo(address, address.additionalChars) // no way to know when they address ends and dirt begins
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -596,17 +596,6 @@ object LocalCache {
|
|||||||
|
|
||||||
fun consume(event: LnZapEvent) {
|
fun consume(event: LnZapEvent) {
|
||||||
val note = getOrCreateNote(event.id)
|
val note = getOrCreateNote(event.id)
|
||||||
|
|
||||||
var decryptedContent = LnZapRequestEvent.checkForPrivateZap(event.zapRequest!!, account.loggedIn.privKey!!)
|
|
||||||
if (decryptedContent != null) {
|
|
||||||
Log.e(
|
|
||||||
"DC",
|
|
||||||
"Decrypted Content from Anon Tag: Sender: {${decryptedContent.pubKey}}, Message: {${decryptedContent.content}} "
|
|
||||||
|
|
||||||
// TODO Update Notification with this Sender and Message
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
@@ -20,6 +21,7 @@ val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
|
|||||||
|
|
||||||
class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
||||||
override fun idNote() = address.toNAddr()
|
override fun idNote() = address.toNAddr()
|
||||||
|
override fun toNEvent() = address.toNAddr()
|
||||||
override fun idDisplayNote() = idNote().toShortenHex()
|
override fun idDisplayNote() = idNote().toShortenHex()
|
||||||
override fun address() = address
|
override fun address() = address
|
||||||
override fun createdAt() = (event as? LongTextNoteEvent)?.publishedAt() ?: event?.createdAt()
|
override fun createdAt() = (event as? LongTextNoteEvent)?.publishedAt() ?: event?.createdAt()
|
||||||
@@ -51,6 +53,11 @@ open class Note(val idHex: String) {
|
|||||||
|
|
||||||
fun id() = Hex.decode(idHex)
|
fun id() = Hex.decode(idHex)
|
||||||
open fun idNote() = id().toNote()
|
open fun idNote() = id().toNote()
|
||||||
|
|
||||||
|
open fun toNEvent(): String {
|
||||||
|
return Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
open fun idDisplayNote() = idNote().toShortenHex()
|
open fun idDisplayNote() = idNote().toShortenHex()
|
||||||
|
|
||||||
fun channelHex(): HexKey? {
|
fun channelHex(): HexKey? {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class User(val pubkeyHex: String) {
|
|||||||
|
|
||||||
fun pubkey() = Hex.decode(pubkeyHex)
|
fun pubkey() = Hex.decode(pubkeyHex)
|
||||||
fun pubkeyNpub() = pubkey().toNpub()
|
fun pubkeyNpub() = pubkey().toNpub()
|
||||||
|
|
||||||
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
|
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
|
||||||
|
|
||||||
override fun toString(): String = pubkeyHex
|
override fun toString(): String = pubkeyHex
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class FileHeader(
|
|||||||
val description: String? = null
|
val description: String? = null
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun prepare(fileUrl: String, mimeType: String?, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
fun prepare(fileUrl: String, mimeType: String?, description: String?, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
||||||
try {
|
try {
|
||||||
val imageData = URL(fileUrl).readBytes()
|
val imageData = URL(fileUrl).readBytes()
|
||||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||||
@@ -55,7 +55,7 @@ class FileHeader(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
onReady(FileHeader(fileUrl, mimeType, hash, size, blurHash, ""))
|
onReady(FileHeader(fileUrl, mimeType, hash, size, blurHash, description))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
||||||
onError()
|
onError()
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.service.model
|
|||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.tagSearch
|
import com.vitorpamplona.amethyst.model.tagSearch
|
||||||
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
|
import com.vitorpamplona.amethyst.service.nip19.Nip19.nip19regex
|
||||||
|
|
||||||
open class BaseTextNoteEvent(
|
open class BaseTextNoteEvent(
|
||||||
id: HexKey,
|
id: HexKey,
|
||||||
@@ -31,6 +33,28 @@ open class BaseTextNoteEvent(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val matcher2 = nip19regex.matcher(content)
|
||||||
|
while (matcher2.find()) {
|
||||||
|
val uriScheme = matcher2.group(1) // nostr:
|
||||||
|
val type = matcher2.group(2) // npub1
|
||||||
|
val key = matcher2.group(3) // bech32
|
||||||
|
val additionalChars = matcher2.group(4) // additional chars
|
||||||
|
|
||||||
|
val parsed = Nip19.parseComponents(uriScheme, type, key, additionalChars)
|
||||||
|
|
||||||
|
if (parsed != null) {
|
||||||
|
try {
|
||||||
|
val tag = tags.firstOrNull { it.size > 1 && it[1] == parsed.hex }
|
||||||
|
|
||||||
|
if (tag != null && tag[0] == "p") {
|
||||||
|
returningList.add(tag[1])
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
citedUsersCache = returningList
|
citedUsersCache = returningList
|
||||||
return returningList
|
return returningList
|
||||||
}
|
}
|
||||||
@@ -51,6 +75,35 @@ open class BaseTextNoteEvent(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val matcher2 = nip19regex.matcher(content)
|
||||||
|
while (matcher2.find()) {
|
||||||
|
val uriScheme = matcher2.group(1) // nostr:
|
||||||
|
val type = matcher2.group(2) // npub1
|
||||||
|
val key = matcher2.group(3) // bech32
|
||||||
|
val additionalChars = matcher2.group(4) // additional chars
|
||||||
|
|
||||||
|
val parsed = Nip19.parseComponents(uriScheme, type, key, additionalChars)
|
||||||
|
|
||||||
|
if (parsed != null) {
|
||||||
|
if (content.contains("Testing event")) {
|
||||||
|
println("AAAA $key")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val tag = tags.firstOrNull { it.size > 1 && it[1] == parsed.hex }
|
||||||
|
|
||||||
|
if (tag != null && tag[0] == "e") {
|
||||||
|
citations.add(tag[1])
|
||||||
|
}
|
||||||
|
if (tag != null && tag[0] == "a") {
|
||||||
|
citations.add(tag[1])
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return citations
|
return citations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ open class Event(
|
|||||||
@SerializedName("created_at") val createdAt: Long,
|
@SerializedName("created_at") val createdAt: Long,
|
||||||
val kind: Int,
|
val kind: Int,
|
||||||
val tags: List<List<String>>,
|
val tags: List<List<String>>,
|
||||||
val content: String,
|
var content: String,
|
||||||
val sig: HexKey
|
val sig: HexKey
|
||||||
) : EventInterface {
|
) : EventInterface {
|
||||||
override fun id(): HexKey = id
|
override fun id(): HexKey = id
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.vitorpamplona.amethyst.service.nip19
|
package com.vitorpamplona.amethyst.service.nip19
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import nostr.postr.Bech32
|
||||||
import nostr.postr.bechToBytes
|
import nostr.postr.bechToBytes
|
||||||
|
import nostr.postr.toByteArray
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
object Nip19 {
|
object Nip19 {
|
||||||
@@ -12,7 +15,14 @@ object Nip19 {
|
|||||||
|
|
||||||
val nip19regex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)(.*)", Pattern.CASE_INSENSITIVE)
|
val nip19regex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)(.*)", Pattern.CASE_INSENSITIVE)
|
||||||
|
|
||||||
data class Return(val type: Type, val hex: String, val relay: String? = null, val additionalChars: String = "")
|
data class Return(
|
||||||
|
val type: Type,
|
||||||
|
val hex: String,
|
||||||
|
val relay: String? = null,
|
||||||
|
val author: String? = null,
|
||||||
|
val kind: Long? = null,
|
||||||
|
val additionalChars: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
fun uriToRoute(uri: String?): Return? {
|
fun uriToRoute(uri: String?): Return? {
|
||||||
if (uri == null) return null
|
if (uri == null) return null
|
||||||
@@ -26,8 +36,23 @@ object Nip19 {
|
|||||||
val uriScheme = matcher.group(1) // nostr:
|
val uriScheme = matcher.group(1) // nostr:
|
||||||
val type = matcher.group(2) // npub1
|
val type = matcher.group(2) // npub1
|
||||||
val key = matcher.group(3) // bech32
|
val key = matcher.group(3) // bech32
|
||||||
val additionalChars = matcher.group(4) ?: "" // additional chars
|
val additionalChars = matcher.group(4) // additional chars
|
||||||
|
|
||||||
|
return parseComponents(uriScheme, type, key, additionalChars)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseComponents(
|
||||||
|
uriScheme: String?,
|
||||||
|
type: String,
|
||||||
|
key: String?,
|
||||||
|
additionalChars: String?
|
||||||
|
): Return? {
|
||||||
|
return try {
|
||||||
val bytes = (type + key).bechToBytes()
|
val bytes = (type + key).bechToBytes()
|
||||||
val parsed = when (type.lowercase()) {
|
val parsed = when (type.lowercase()) {
|
||||||
"npub1" -> npub(bytes)
|
"npub1" -> npub(bytes)
|
||||||
@@ -38,12 +63,11 @@ object Nip19 {
|
|||||||
"naddr1" -> naddr(bytes)
|
"naddr1" -> naddr(bytes)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
return parsed?.copy(additionalChars = additionalChars)
|
parsed?.copy(additionalChars = additionalChars ?: "")
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e)
|
Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun npub(bytes: ByteArray): Return {
|
private fun npub(bytes: ByteArray): Return {
|
||||||
@@ -79,7 +103,15 @@ object Nip19 {
|
|||||||
?.get(0)
|
?.get(0)
|
||||||
?.toString(Charsets.UTF_8)
|
?.toString(Charsets.UTF_8)
|
||||||
|
|
||||||
return Return(Type.EVENT, hex, relay)
|
val author = tlv.get(Tlv.Type.AUTHOR.id)
|
||||||
|
?.get(0)
|
||||||
|
?.toHexKey()
|
||||||
|
|
||||||
|
val kind = tlv.get(Tlv.Type.KIND.id)
|
||||||
|
?.get(0)
|
||||||
|
?.let { Tlv.toInt32(it) }?.toLong()
|
||||||
|
|
||||||
|
return Return(Type.EVENT, hex, relay, author, kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun nrelay(bytes: ByteArray): Return? {
|
private fun nrelay(bytes: ByteArray): Return? {
|
||||||
@@ -108,8 +140,31 @@ object Nip19 {
|
|||||||
|
|
||||||
val kind = tlv.get(Tlv.Type.KIND.id)
|
val kind = tlv.get(Tlv.Type.KIND.id)
|
||||||
?.get(0)
|
?.get(0)
|
||||||
?.let { Tlv.toInt32(it) }
|
?.let { Tlv.toInt32(it) }?.toLong()
|
||||||
|
|
||||||
return Return(Type.ADDRESS, "$kind:$author:$d", relay)
|
return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun createNEvent(idHex: String, author: String?, kind: Int?, relay: String?): String {
|
||||||
|
val kind = kind?.toByteArray()
|
||||||
|
val author = author?.toByteArray()
|
||||||
|
val idHex = idHex.toByteArray()
|
||||||
|
val relay = relay?.toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
|
var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, idHex.size.toByte()) + idHex
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
fullArray = fullArray + byteArrayOf(Tlv.Type.RELAY.id, relay.size.toByte()) + relay
|
||||||
|
}
|
||||||
|
|
||||||
|
if (author != null) {
|
||||||
|
fullArray = fullArray + byteArrayOf(Tlv.Type.AUTHOR.id, author.size.toByte()) + author
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind != null) {
|
||||||
|
fullArray = fullArray + byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bech32.encodeBytes(hrp = "nevent", fullArray, Bech32.Encoding.Bech32)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,19 +56,19 @@ class NewMessageTagger(var channel: Channel?, var mentions: List<User>?, var rep
|
|||||||
if (results?.key?.type == Nip19.Type.USER) {
|
if (results?.key?.type == Nip19.Type.USER) {
|
||||||
val user = LocalCache.getOrCreateUser(results.key.hex)
|
val user = LocalCache.getOrCreateUser(results.key.hex)
|
||||||
|
|
||||||
"#[${tagIndex(user)}]${results.restOfWord}"
|
"nostr:${user.pubkeyNpub()}${results.restOfWord}"
|
||||||
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
||||||
val note = LocalCache.getOrCreateNote(results.key.hex)
|
val note = LocalCache.getOrCreateNote(results.key.hex)
|
||||||
|
|
||||||
"#[${tagIndex(note)}]${results.restOfWord}"
|
"nostr:${note.toNEvent()}${results.restOfWord}"
|
||||||
} else if (results?.key?.type == Nip19.Type.EVENT) {
|
} else if (results?.key?.type == Nip19.Type.EVENT) {
|
||||||
val note = LocalCache.getOrCreateNote(results.key.hex)
|
val note = LocalCache.getOrCreateNote(results.key.hex)
|
||||||
|
|
||||||
"#[${tagIndex(note)}]${results.restOfWord}"
|
"nostr:${note.toNEvent()}${results.restOfWord}"
|
||||||
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
|
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
|
||||||
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
|
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
|
||||||
if (note != null) {
|
if (note != null) {
|
||||||
"#[${tagIndex(note)}]${results.restOfWord}"
|
"nostr:${note.idNote()}${results.restOfWord}"
|
||||||
} else {
|
} else {
|
||||||
word
|
word
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.actions
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Size
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -13,10 +17,15 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
import androidx.compose.material.icons.filled.CurrencyBitcoin
|
import androidx.compose.material.icons.filled.CurrencyBitcoin
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -25,18 +34,22 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextDirection
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
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 androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
@@ -44,13 +57,16 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.ui.components.*
|
import com.vitorpamplona.amethyst.ui.components.*
|
||||||
import com.vitorpamplona.amethyst.ui.note.ReplyInformation
|
import com.vitorpamplona.amethyst.ui.note.ReplyInformation
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account) {
|
fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
val postViewModel: NewPostViewModel = viewModel()
|
val postViewModel: NewPostViewModel = viewModel()
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -186,6 +202,19 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val url = postViewModel.contentToAddUrl
|
||||||
|
if (url != null) {
|
||||||
|
ImageVideoDescription(
|
||||||
|
url,
|
||||||
|
onAdd = { description ->
|
||||||
|
postViewModel.upload(url, description, context)
|
||||||
|
},
|
||||||
|
onCancel = {
|
||||||
|
postViewModel.contentToAddUrl = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val user = postViewModel.account?.userProfile()
|
val user = postViewModel.account?.userProfile()
|
||||||
val lud16 = user?.info?.lnAddress()
|
val lud16 = user?.info?.lnAddress()
|
||||||
|
|
||||||
@@ -234,6 +263,14 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||||||
} else {
|
} else {
|
||||||
UrlPreview(myUrlPreview, myUrlPreview)
|
UrlPreview(myUrlPreview, myUrlPreview)
|
||||||
}
|
}
|
||||||
|
} else if (isBechLink(myUrlPreview)) {
|
||||||
|
BechLink(
|
||||||
|
myUrlPreview,
|
||||||
|
true,
|
||||||
|
MaterialTheme.colors.background,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
} else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) {
|
} else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) {
|
||||||
UrlPreview("https://$myUrlPreview", myUrlPreview)
|
UrlPreview("https://$myUrlPreview", myUrlPreview)
|
||||||
}
|
}
|
||||||
@@ -267,7 +304,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||||||
tint = MaterialTheme.colors.onBackground,
|
tint = MaterialTheme.colors.onBackground,
|
||||||
modifier = Modifier.padding(bottom = 10.dp)
|
modifier = Modifier.padding(bottom = 10.dp)
|
||||||
) {
|
) {
|
||||||
postViewModel.upload(it, context)
|
postViewModel.selectImage(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postViewModel.canUsePoll) {
|
if (postViewModel.canUsePoll) {
|
||||||
@@ -447,3 +484,149 @@ fun SearchButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImageVideoDescription(
|
||||||
|
uri: Uri,
|
||||||
|
onAdd: (String) -> Unit,
|
||||||
|
onCancel: () -> Unit
|
||||||
|
) {
|
||||||
|
val resolver = LocalContext.current.contentResolver
|
||||||
|
val mediaType = resolver.getType(uri) ?: ""
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val isImage = mediaType.startsWith("image")
|
||||||
|
val isVideo = mediaType.startsWith("video")
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 30.dp, end = 30.dp)
|
||||||
|
.clip(shape = RoundedCornerShape(10.dp))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||||
|
RoundedCornerShape(15.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(30.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 10.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
if (isImage) {
|
||||||
|
R.string.content_description_add_image
|
||||||
|
} else {
|
||||||
|
if (isVideo) {
|
||||||
|
R.string.content_description_add_video
|
||||||
|
} else {
|
||||||
|
R.string.content_description_add_document
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.W500,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(1.0f)
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(30.dp),
|
||||||
|
onClick = onCancel
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Cancel,
|
||||||
|
null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 5.dp)
|
||||||
|
.size(30.dp),
|
||||||
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 10.dp)
|
||||||
|
) {
|
||||||
|
if (mediaType.startsWith("image")) {
|
||||||
|
AsyncImage(
|
||||||
|
model = uri.toString(),
|
||||||
|
contentDescription = uri.toString(),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
} else if (mediaType.startsWith("video") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = uri) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
bitmap = resolver.loadThumbnail(uri, Size(1200, 1000), null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitmap?.let {
|
||||||
|
Image(
|
||||||
|
bitmap = it.asImageBitmap(),
|
||||||
|
contentDescription = "some useful description",
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VideoView(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var message by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringResource(R.string.content_description)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = message,
|
||||||
|
onValueChange = { message = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.content_description_example),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 10.dp),
|
||||||
|
onClick = {
|
||||||
|
onAdd(message)
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(15.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.add_content), color = Color.White, fontSize = 20.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
var userSuggestions by mutableStateOf<List<User>>(emptyList())
|
var userSuggestions by mutableStateOf<List<User>>(emptyList())
|
||||||
var userSuggestionAnchor: TextRange? = null
|
var userSuggestionAnchor: TextRange? = null
|
||||||
|
|
||||||
|
// Images and Videos
|
||||||
|
var contentToAddUrl by mutableStateOf<Uri?>(null)
|
||||||
|
|
||||||
// Polls
|
// Polls
|
||||||
var canUsePoll by mutableStateOf(false)
|
var canUsePoll by mutableStateOf(false)
|
||||||
var wantsPoll by mutableStateOf(false)
|
var wantsPoll by mutableStateOf(false)
|
||||||
@@ -79,11 +82,12 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
quote?.let {
|
quote?.let {
|
||||||
message = TextFieldValue(message.text + "\n\n@${it.idNote()}")
|
message = TextFieldValue(message.text + "\n\n@${it.toNEvent()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
canAddInvoice = account.userProfile().info?.lnAddress() != null
|
canAddInvoice = account.userProfile().info?.lnAddress() != null
|
||||||
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null
|
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null
|
||||||
|
contentToAddUrl = null
|
||||||
|
|
||||||
this.account = account
|
this.account = account
|
||||||
}
|
}
|
||||||
@@ -105,46 +109,15 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upload(it: Uri, context: Context) {
|
fun upload(it: Uri, description: String, context: Context) {
|
||||||
isUploadingImage = true
|
isUploadingImage = true
|
||||||
|
contentToAddUrl = null
|
||||||
|
|
||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
uri = it,
|
uri = it,
|
||||||
contentResolver = context.contentResolver,
|
contentResolver = context.contentResolver,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
onSuccess = { imageUrl, mimeType ->
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
createNIP97Record(imageUrl, mimeType, description)
|
||||||
// Images don't seem to be ready immediately after upload
|
|
||||||
|
|
||||||
if (mimeType?.startsWith("image/") == true) {
|
|
||||||
delay(2000)
|
|
||||||
} else {
|
|
||||||
delay(5000)
|
|
||||||
}
|
|
||||||
|
|
||||||
FileHeader.prepare(
|
|
||||||
imageUrl,
|
|
||||||
mimeType,
|
|
||||||
onReady = {
|
|
||||||
val note = account?.sendHeader(it)
|
|
||||||
|
|
||||||
isUploadingImage = false
|
|
||||||
|
|
||||||
if (note == null) {
|
|
||||||
message = TextFieldValue(message.text + "\n\n" + imageUrl)
|
|
||||||
} else {
|
|
||||||
message = TextFieldValue(message.text + "\n\nnostr:" + note.idNote())
|
|
||||||
}
|
|
||||||
|
|
||||||
urlPreview = findUrlInMessage()
|
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
isUploadingImage = false
|
|
||||||
viewModelScope.launch {
|
|
||||||
imageUploadingError.emit("Failed to upload the image / video")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
@@ -157,6 +130,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
|
|
||||||
open fun cancel() {
|
open fun cancel() {
|
||||||
message = TextFieldValue("")
|
message = TextFieldValue("")
|
||||||
|
contentToAddUrl = null
|
||||||
urlPreview = null
|
urlPreview = null
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
mentions = null
|
mentions = null
|
||||||
@@ -220,7 +194,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun canPost(): Boolean {
|
fun canPost(): Boolean {
|
||||||
return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice &&
|
return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice &&
|
||||||
(!wantsPoll || pollOptions.values.all { it.isNotEmpty() })
|
(!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) && contentToAddUrl == null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun includePollHashtagInMessage(include: Boolean, hashtag: String) {
|
fun includePollHashtagInMessage(include: Boolean, hashtag: String) {
|
||||||
@@ -235,4 +209,45 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createNIP97Record(imageUrl: String, mimeType: String?, description: String) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
// Images don't seem to be ready immediately after upload
|
||||||
|
|
||||||
|
if (mimeType?.startsWith("image/") == true) {
|
||||||
|
delay(2000)
|
||||||
|
} else {
|
||||||
|
delay(5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
FileHeader.prepare(
|
||||||
|
imageUrl,
|
||||||
|
mimeType,
|
||||||
|
description,
|
||||||
|
onReady = {
|
||||||
|
val note = account?.sendHeader(it)
|
||||||
|
|
||||||
|
isUploadingImage = false
|
||||||
|
|
||||||
|
if (note == null) {
|
||||||
|
message = TextFieldValue(message.text + "\n\n" + imageUrl)
|
||||||
|
} else {
|
||||||
|
message = TextFieldValue(message.text + "\n\nnostr:" + note.toNEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
urlPreview = findUrlInMessage()
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
isUploadingImage = false
|
||||||
|
viewModelScope.launch {
|
||||||
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectImage(uri: Uri) {
|
||||||
|
contentToAddUrl = uri
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPollView
|
import com.vitorpamplona.amethyst.ui.actions.NewPollView
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FabColumn(account: Account) {
|
fun FabColumn(account: Account) {
|
||||||
@@ -88,7 +86,7 @@ fun FabColumn(account: Account) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToPost) {
|
if (wantsToPost) {
|
||||||
NewPostView({ wantsToPost = false }, account = NostrAccountDataSource.account)
|
// NewPostView({ wantsToPost = false }, account = NostrAccountDataSource.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToPoll) {
|
if (wantsToPoll) {
|
||||||
|
|||||||
@@ -16,18 +16,20 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewNoteButton(account: Account) {
|
fun NewNoteButton(account: Account, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
var wantsToPost by remember {
|
var wantsToPost by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToPost) {
|
if (wantsToPost) {
|
||||||
NewPostView({ wantsToPost = false }, account = account)
|
NewPostView({ wantsToPost = false }, account = account, accountViewModel = accountViewModel, navController = navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import androidx.compose.ui.unit.sp
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -136,7 +135,7 @@ fun InvoiceRequest(
|
|||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
|
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, LnZapEvent.ZapType.PUBLIC)
|
val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType)
|
||||||
|
|
||||||
LightningAddressResolver().lnAddressInvoice(
|
LightningAddressResolver().lnAddressInvoice(
|
||||||
lud16,
|
lud16,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -16,6 +17,7 @@ import androidx.lifecycle.LifecycleEventObserver
|
|||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.ExoPlayer
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
|
import com.google.android.exoplayer2.MediaMetadata
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||||
@@ -23,16 +25,33 @@ import com.google.android.exoplayer2.ui.StyledPlayerView
|
|||||||
import com.vitorpamplona.amethyst.VideoCache
|
import com.vitorpamplona.amethyst.VideoCache
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoView(videoUri: String, onDialog: ((Boolean) -> Unit)? = null) {
|
fun VideoView(videoUri: String, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) {
|
||||||
|
VideoView(Uri.parse(videoUri), description, onDialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoView(videoUri: Uri, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
|
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
|
||||||
|
|
||||||
val exoPlayer = remember(videoUri) {
|
val exoPlayer = remember(videoUri) {
|
||||||
|
val mediaBuilder = MediaItem.Builder().setUri(videoUri)
|
||||||
|
|
||||||
|
description?.let {
|
||||||
|
mediaBuilder.setMediaMetadata(
|
||||||
|
MediaMetadata.Builder().setDisplayTitle(it).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val media = mediaBuilder.build()
|
||||||
|
|
||||||
ExoPlayer.Builder(context).build().apply {
|
ExoPlayer.Builder(context).build().apply {
|
||||||
repeatMode = Player.REPEAT_MODE_ALL
|
repeatMode = Player.REPEAT_MODE_ALL
|
||||||
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
|
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
|
||||||
setMediaSource(
|
setMediaSource(
|
||||||
ProgressiveMediaSource.Factory(VideoCache.get()).createMediaSource(MediaItem.fromUri(videoUri))
|
ProgressiveMediaSource.Factory(VideoCache.get()).createMediaSource(
|
||||||
|
media
|
||||||
|
)
|
||||||
)
|
)
|
||||||
prepare()
|
prepare()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ fun ZoomableContentView(content: ZoomableContent, images: List<ZoomableContent>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
VideoView(content.url) { dialogOpen = true }
|
VideoView(content.url, content.description) { dialogOpen = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
@@ -322,7 +322,7 @@ private fun RenderImageOrVideo(content: ZoomableContent) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
|
||||||
VideoView(content.url)
|
VideoView(content.url, content.description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,13 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
@@ -135,7 +140,16 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthorGallery(multiSetCard.zapEvents.keys, navController, account)
|
for (i in multiSetCard.zapEvents) {
|
||||||
|
var decryptedContent = (i.value.event as LnZapEvent).zapRequest?.let {
|
||||||
|
LnZapRequestEvent.checkForPrivateZap(it, NostrAccountDataSource.account.loggedIn.privKey!!)
|
||||||
|
}
|
||||||
|
if (decryptedContent != null) {
|
||||||
|
(i.key.event as Event).content = decryptedContent.content
|
||||||
|
i.key.author = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AuthorGallery(multiSetCard.zapEvents.keys, navController, account, accountViewModel, "zap")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +170,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthorGallery(multiSetCard.boostEvents, navController, account)
|
AuthorGallery(multiSetCard.boostEvents, navController, account, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +191,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthorGallery(multiSetCard.likeEvents, navController, account)
|
AuthorGallery(multiSetCard.likeEvents, navController, account, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +225,9 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
|||||||
fun AuthorGallery(
|
fun AuthorGallery(
|
||||||
authorNotes: Collection<Note>,
|
authorNotes: Collection<Note>,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
account: Account
|
account: Account,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
kind: String = "nonzap"
|
||||||
) {
|
) {
|
||||||
val accountState by account.userProfile().live().follows.observeAsState()
|
val accountState by account.userProfile().live().follows.observeAsState()
|
||||||
val accountUser = accountState?.user ?: return
|
val accountUser = accountState?.user ?: return
|
||||||
@@ -219,12 +235,39 @@ fun AuthorGallery(
|
|||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
authorNotes.forEach {
|
authorNotes.forEach {
|
||||||
FastNoteAuthorPicture(
|
if (it.event?.content() != "" && kind == "zap") {
|
||||||
note = it,
|
Row(Modifier.fillMaxWidth()) {
|
||||||
navController = navController,
|
FastNoteAuthorPicture(
|
||||||
userAccount = accountUser,
|
note = it,
|
||||||
size = 35.dp
|
navController = navController,
|
||||||
)
|
userAccount = accountUser,
|
||||||
|
size = 35.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row() {
|
||||||
|
FastNoteAuthorPicture(
|
||||||
|
note = it,
|
||||||
|
navController = navController,
|
||||||
|
userAccount = accountUser,
|
||||||
|
size = 35.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (it.event?.content() != "" && kind == "zap") {
|
||||||
|
Row(Modifier.fillMaxWidth()) {
|
||||||
|
it.event?.let {
|
||||||
|
TranslatableRichTextViewer(
|
||||||
|
content = it.content(),
|
||||||
|
canPreview = true,
|
||||||
|
tags = null,
|
||||||
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +288,6 @@ fun FastNoteAuthorPicture(
|
|||||||
val user = userState?.user ?: return
|
val user = userState?.user ?: return
|
||||||
|
|
||||||
val showFollowingMark = userAccount.isFollowingCached(user) || user === userAccount
|
val showFollowingMark = userAccount.isFollowingCached(user) || user === userAccount
|
||||||
|
|
||||||
UserPicture(
|
UserPicture(
|
||||||
userHex = user.pubkeyHex,
|
userHex = user.pubkeyHex,
|
||||||
userPicture = user.profilePicture(),
|
userPicture = user.profilePicture(),
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ fun NoteComposeInner(
|
|||||||
} else if (noteEvent is LongTextNoteEvent) {
|
} else if (noteEvent is LongTextNoteEvent) {
|
||||||
LongFormHeader(noteEvent, note, loggedIn)
|
LongFormHeader(noteEvent, note, loggedIn)
|
||||||
|
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel, navController)
|
||||||
|
|
||||||
Divider(
|
Divider(
|
||||||
modifier = Modifier.padding(top = 10.dp),
|
modifier = Modifier.padding(top = 10.dp),
|
||||||
@@ -485,7 +485,7 @@ fun NoteComposeInner(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel, navController)
|
||||||
|
|
||||||
Divider(
|
Divider(
|
||||||
modifier = Modifier.padding(top = 10.dp),
|
modifier = Modifier.padding(top = 10.dp),
|
||||||
@@ -512,7 +512,7 @@ fun NoteComposeInner(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!makeItShort) {
|
if (!makeItShort) {
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel, navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider(
|
Divider(
|
||||||
@@ -556,7 +556,7 @@ fun NoteComposeInner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!makeItShort) {
|
if (!makeItShort) {
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel, navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider(
|
Divider(
|
||||||
@@ -1196,10 +1196,10 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
|||||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) {
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) {
|
||||||
Text(stringResource(R.string.copy_text))
|
Text(stringResource(R.string.copy_text))
|
||||||
}
|
}
|
||||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("@${note.author?.pubkeyNpub()}")); onDismiss() }) {
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}")); onDismiss() }) {
|
||||||
Text(stringResource(R.string.copy_user_pubkey))
|
Text(stringResource(R.string.copy_user_pubkey))
|
||||||
}
|
}
|
||||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); onDismiss() }) {
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent())); onDismiss() }) {
|
||||||
Text(stringResource(R.string.copy_note_id))
|
Text(stringResource(R.string.copy_note_id))
|
||||||
}
|
}
|
||||||
DropdownMenuItem(onClick = {
|
DropdownMenuItem(onClick = {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ val externalLinkForNote = { note: Note ->
|
|||||||
"https://habla.news/a/${note.address().toNAddr()}"
|
"https://habla.news/a/${note.address().toNAddr()}"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
"https://snort.social/e/${note.idNote()}"
|
"https://snort.social/e/${note.toNEvent()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,13 +152,13 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
|
|||||||
}
|
}
|
||||||
VerticalDivider(primaryLight)
|
VerticalDivider(primaryLight)
|
||||||
NoteQuickActionItem(Icons.Default.AlternateEmail, stringResource(R.string.quick_action_copy_user_id)) {
|
NoteQuickActionItem(Icons.Default.AlternateEmail, stringResource(R.string.quick_action_copy_user_id)) {
|
||||||
clipboardManager.setText(AnnotatedString("@${note.author?.pubkeyNpub()}"))
|
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}"))
|
||||||
showToast(R.string.copied_user_id_to_clipboard)
|
showToast(R.string.copied_user_id_to_clipboard)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
VerticalDivider(primaryLight)
|
VerticalDivider(primaryLight)
|
||||||
NoteQuickActionItem(Icons.Default.FormatQuote, stringResource(R.string.quick_action_copy_note_id)) {
|
NoteQuickActionItem(Icons.Default.FormatQuote, stringResource(R.string.quick_action_copy_note_id)) {
|
||||||
clipboardManager.setText(AnnotatedString("@${note.idNote()}"))
|
clipboardManager.setText(AnnotatedString("nostr:${note.toNEvent()}"))
|
||||||
showToast(R.string.copied_note_id_to_clipboard)
|
showToast(R.string.copied_note_id_to_clipboard)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ fun ZapVote(
|
|||||||
zappingProgress = it
|
zappingProgress = it
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
zapType = LnZapEvent.ZapType.PUBLIC
|
zapType = account.defaultZapType
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ import androidx.compose.ui.unit.IntOffset
|
|||||||
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.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
|
import androidx.navigation.NavController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.CachePolicy
|
import coil.request.CachePolicy
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
@@ -66,7 +66,7 @@ import java.math.RoundingMode
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
@@ -79,11 +79,11 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToReplyTo != null) {
|
if (wantsToReplyTo != null) {
|
||||||
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account)
|
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wantsToQuote != null) {
|
if (wantsToQuote != null) {
|
||||||
NewPostView({ wantsToQuote = null }, null, wantsToQuote, account)
|
NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -364,7 +364,7 @@ fun ZapReaction(
|
|||||||
zappingProgress = it
|
zappingProgress = it
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
zapType = LnZapEvent.ZapType.PUBLIC
|
zapType = account.defaultZapType
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (account.zapAmountChoices.size > 1) {
|
} else if (account.zapAmountChoices.size > 1) {
|
||||||
@@ -561,7 +561,7 @@ fun ZapAmountChoicePopup(
|
|||||||
context,
|
context,
|
||||||
onError,
|
onError,
|
||||||
onProgress,
|
onProgress,
|
||||||
LnZapEvent.ZapType.PUBLIC
|
account.defaultZapType
|
||||||
)
|
)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
@@ -587,7 +587,7 @@ fun ZapAmountChoicePopup(
|
|||||||
context,
|
context,
|
||||||
onError,
|
onError,
|
||||||
onProgress,
|
onProgress,
|
||||||
LnZapEvent.ZapType.PUBLIC
|
account.defaultZapType
|
||||||
)
|
)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
class ZapOptionstViewModel : ViewModel() {
|
class ZapOptionstViewModel : ViewModel() {
|
||||||
private var account: Account? = null
|
private var account: Account? = null
|
||||||
|
|
||||||
var customAmount by mutableStateOf(TextFieldValue("21"))
|
var customAmount by mutableStateOf(TextFieldValue("21"))
|
||||||
var customMessage by mutableStateOf(TextFieldValue(""))
|
var customMessage by mutableStateOf(TextFieldValue(""))
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc
|
|||||||
)
|
)
|
||||||
|
|
||||||
val zapOptions = zapTypes.map { it.second }
|
val zapOptions = zapTypes.map { it.second }
|
||||||
var selectedZapType by remember { mutableStateOf(zapTypes[0]) }
|
var selectedZapType by remember { mutableStateOf(account.defaultZapType) }
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = { onClose() },
|
onDismissRequest = { onClose() },
|
||||||
@@ -116,7 +117,7 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc
|
|||||||
zappingProgress = it
|
zappingProgress = it
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
zapType = selectedZapType.first
|
zapType = selectedZapType
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClose()
|
onClose()
|
||||||
@@ -163,7 +164,15 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc
|
|||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
// stringResource(R.string.new_amount_in_sats
|
// stringResource(R.string.new_amount_in_sats
|
||||||
label = { Text(text = stringResource(id = R.string.custom_zaps_add_a_message)) },
|
label = {
|
||||||
|
if (selectedZapType == LnZapEvent.ZapType.PUBLIC || selectedZapType == LnZapEvent.ZapType.ANONYMOUS) {
|
||||||
|
Text(text = stringResource(id = R.string.custom_zaps_add_a_message))
|
||||||
|
} else if (selectedZapType == LnZapEvent.ZapType.PRIVATE) {
|
||||||
|
Text(text = stringResource(id = R.string.custom_zaps_add_a_message_private))
|
||||||
|
} else if (selectedZapType == LnZapEvent.ZapType.NONZAP) {
|
||||||
|
Text(text = stringResource(id = R.string.custom_zaps_add_a_message_nonzap))
|
||||||
|
}
|
||||||
|
},
|
||||||
value = postViewModel.customMessage,
|
value = postViewModel.customMessage,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
postViewModel.customMessage = it
|
postViewModel.customMessage = it
|
||||||
@@ -186,10 +195,11 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc
|
|||||||
}
|
}
|
||||||
TextSpinner(
|
TextSpinner(
|
||||||
label = "Zap Type",
|
label = "Zap Type",
|
||||||
placeholder = "Public",
|
placeholder = zapTypes.filter { it.first == account.defaultZapType }.first().second,
|
||||||
options = zapOptions,
|
options = zapOptions,
|
||||||
onSelect = {
|
onSelect = {
|
||||||
selectedZapType = zapTypes[it]
|
selectedZapType = zapTypes[it].first
|
||||||
|
account.changeDefaultZapType(selectedZapType)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ fun NoteMaster(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel, navController)
|
||||||
|
|
||||||
Divider(
|
Divider(
|
||||||
modifier = Modifier.padding(top = 10.dp),
|
modifier = Modifier.padding(top = 10.dp),
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ fun ChannelScreen(
|
|||||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
modifier = Modifier.padding(start = 5.dp)
|
modifier = Modifier.padding(start = 5.dp)
|
||||||
) {
|
) {
|
||||||
channelScreenModel.upload(it, context)
|
channelScreenModel.upload(it, "", context)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
|
|||||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
modifier = Modifier.padding(start = 5.dp)
|
modifier = Modifier.padding(start = 5.dp)
|
||||||
) {
|
) {
|
||||||
chatRoomScreenModel.upload(it, context)
|
chatRoomScreenModel.upload(it, "", context)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingButtons(navController, accountStateViewModel)
|
FloatingButtons(navController, accountViewModel, accountStateViewModel)
|
||||||
},
|
},
|
||||||
scaffoldState = scaffoldState
|
scaffoldState = scaffoldState
|
||||||
) {
|
) {
|
||||||
@@ -85,8 +85,8 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FloatingButtons(navController: NavHostController, accountViewModel: AccountStateViewModel) {
|
fun FloatingButtons(navController: NavHostController, accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel) {
|
||||||
val accountState by accountViewModel.accountContent.collectAsState()
|
val accountState by accountStateViewModel.accountContent.collectAsState()
|
||||||
|
|
||||||
if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) {
|
if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) {
|
||||||
Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state ->
|
Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state ->
|
||||||
@@ -98,7 +98,7 @@ fun FloatingButtons(navController: NavHostController, accountViewModel: AccountS
|
|||||||
// Does nothing.
|
// Does nothing.
|
||||||
}
|
}
|
||||||
is AccountState.LoggedIn -> {
|
is AccountState.LoggedIn -> {
|
||||||
NewNoteButton(state.account)
|
NewNoteButton(state.account, accountViewModel, navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,6 +287,9 @@
|
|||||||
<string name="looking_for_event">"Looking for Event %1$s"</string>
|
<string name="looking_for_event">"Looking for Event %1$s"</string>
|
||||||
|
|
||||||
<string name="custom_zaps_add_a_message">Add a public message</string>
|
<string name="custom_zaps_add_a_message">Add a public message</string>
|
||||||
|
<string name="custom_zaps_add_a_message_private">Add a private message</string>
|
||||||
|
<string name="custom_zaps_add_a_message_nonzap">Add an invoice message</string>
|
||||||
|
|
||||||
<string name="custom_zaps_add_a_message_example">Thank you for all your work!</string>
|
<string name="custom_zaps_add_a_message_example">Thank you for all your work!</string>
|
||||||
|
|
||||||
<string name="lightning_create_and_add_invoice">Create and Add</string>
|
<string name="lightning_create_and_add_invoice">Create and Add</string>
|
||||||
@@ -297,4 +300,11 @@
|
|||||||
<string name="hash_verification_passed">Image is the same since the post</string>
|
<string name="hash_verification_passed">Image is the same since the post</string>
|
||||||
<string name="hash_verification_failed">Image has changed. The author might not have seen the change</string>
|
<string name="hash_verification_failed">Image has changed. The author might not have seen the change</string>
|
||||||
|
|
||||||
|
<string name="content_description_add_image">Add Image</string>
|
||||||
|
<string name="content_description_add_video">Add Video</string>
|
||||||
|
<string name="content_description_add_document">Add Document</string>
|
||||||
|
<string name="add_content">Create and Add</string>
|
||||||
|
<string name="content_description">Description of the contents</string>
|
||||||
|
<string name="content_description_example">A blue boat in a white sandy beach at sunset</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -54,12 +54,24 @@ class NIP19ParserTest {
|
|||||||
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address.toNAddr())
|
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address.toNAddr())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nAddrParserPablo() {
|
||||||
|
val result = Nip19.uriToRoute("naddr1qq2hs7p30p6kcunxxamkgcnyd33xxve3veshyq3qyujphdcz69z6jafxpnldae3xtymdekfeatkt3r4qusr3w5krqspqxpqqqpaxjlg805f")
|
||||||
|
assertEquals(Nip19.Type.ADDRESS, result?.type)
|
||||||
|
assertEquals("31337:27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402:xx1xulrf7wdbdlbc31far", result?.hex)
|
||||||
|
assertEquals(null, result?.relay)
|
||||||
|
assertEquals("27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402", result?.author)
|
||||||
|
assertEquals(31337L, result?.kind)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun nAddrParserGizmo() {
|
fun nAddrParserGizmo() {
|
||||||
val result = Nip19.uriToRoute("naddr1qpqrvvfnvccrzdryxgunzvtxvgukge34xfjnqdpcv9sk2desxgmrscesvserzd3h8ycrywphvg6nsvf58ycnqef3v5mnsvt98pjnqdfs8ypzq3huhccxt6h34eupz3jeynjgjgek8lel2f4adaea0svyk94a3njdqvzqqqr4gudhrkyk")
|
val result = Nip19.uriToRoute("naddr1qpqrvvfnvccrzdryxgunzvtxvgukge34xfjnqdpcv9sk2desxgmrscesvserzd3h8ycrywphvg6nsvf58ycnqef3v5mnsvt98pjnqdfs8ypzq3huhccxt6h34eupz3jeynjgjgek8lel2f4adaea0svyk94a3njdqvzqqqr4gudhrkyk")
|
||||||
assertEquals(Nip19.Type.ADDRESS, result?.type)
|
assertEquals(Nip19.Type.ADDRESS, result?.type)
|
||||||
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:613f014d2911fb9df52e048aae70268c0d216790287b5814910e1e781e8e0509", result?.hex)
|
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:613f014d2911fb9df52e048aae70268c0d216790287b5814910e1e781e8e0509", result?.hex)
|
||||||
assertEquals(null, result?.relay)
|
assertEquals(null, result?.relay)
|
||||||
|
assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
|
||||||
|
assertEquals(30023L, result?.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -68,6 +80,8 @@ class NIP19ParserTest {
|
|||||||
assertEquals(Nip19.Type.ADDRESS, result?.type)
|
assertEquals(Nip19.Type.ADDRESS, result?.type)
|
||||||
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418", result?.hex)
|
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418", result?.hex)
|
||||||
assertEquals(null, result?.relay)
|
assertEquals(null, result?.relay)
|
||||||
|
assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
|
||||||
|
assertEquals(30023L, result?.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -76,5 +90,62 @@ class NIP19ParserTest {
|
|||||||
assertEquals(Nip19.Type.EVENT, result?.type)
|
assertEquals(Nip19.Type.EVENT, result?.type)
|
||||||
assertEquals("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", result?.hex)
|
assertEquals("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", result?.hex)
|
||||||
assertEquals(null, result?.relay)
|
assertEquals(null, result?.relay)
|
||||||
|
assertEquals(null, result?.author)
|
||||||
|
assertEquals(null, result?.kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nEventParser() {
|
||||||
|
val result = Nip19.uriToRoute("nostr:nevent1qqstvrl6wftd8ht4g0vrp6m30tjs6pdxcvk977g769dcvlptkzu4ftqppamhxue69uhkummnw3ezumt0d5pzp78lz8r60568sd2a8dx3wnj6gume02gxaf92vx4fk67qv5kpagt6qvzqqqqqqygqr86c")
|
||||||
|
assertEquals(Nip19.Type.EVENT, result?.type)
|
||||||
|
assertEquals("b60ffa7256d3dd7543d830eb717ae50d05a6c32c5f791ed15b867c2bb0b954ac", result?.hex)
|
||||||
|
assertEquals("wss://nostr.mom", result?.relay)
|
||||||
|
assertEquals("f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a", result?.author)
|
||||||
|
assertEquals(1L, result?.kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nEventParser2() {
|
||||||
|
val result = Nip19.uriToRoute("nostr:nevent1qqsplpuwsgrrmq85rfup6w3w777rxmcmadu590emfx6z4msj2844euqpz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqye3a70w")
|
||||||
|
|
||||||
|
assertEquals(Nip19.Type.EVENT, result?.type)
|
||||||
|
assertEquals("1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0", result?.hex)
|
||||||
|
assertEquals("wss://relay.damus.io", result?.relay)
|
||||||
|
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author)
|
||||||
|
assertEquals(1L, result?.kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nEventParserInvalidChecksum() {
|
||||||
|
val result = Nip19.uriToRoute("nostr:nevent1qqsyxq8v0730nz38dupnjzp5jegkyz4gu2ptwcps4v32hjnrap0q0espz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqyn3t9gj")
|
||||||
|
|
||||||
|
assertEquals(Nip19.Type.EVENT, result?.type)
|
||||||
|
assertEquals("4300ec7fa2f98a276f033908349651620aa8e282b76030ab22abca63e85e07e6", result?.hex)
|
||||||
|
assertEquals("wss://relay.damus.io", result?.relay)
|
||||||
|
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author)
|
||||||
|
assertEquals(1L, result?.kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nEventFormatter() {
|
||||||
|
val nevent = Nip19.createNEvent("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", null, null, null)
|
||||||
|
assertEquals("nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy", nevent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nEventFormatterWithExtraInfo() {
|
||||||
|
val nevent = Nip19.createNEvent("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", 40, null)
|
||||||
|
assertEquals("nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhqzypl62m6ad932k83u6sjwwkxrqq4cve0hkrvdem5la83g34m4rtqegqcyqqqqq2qh26va4", nevent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nEventFormatterWithFullInfo() {
|
||||||
|
val nevent = Nip19.createNEvent(
|
||||||
|
"1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0",
|
||||||
|
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||||
|
1,
|
||||||
|
"wss://relay.damus.io"
|
||||||
|
)
|
||||||
|
assertEquals("nevent1qqsplpuwsgrrmq85rfup6w3w777rxmcmadu590emfx6z4msj2844euqpz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqye3a70w", nevent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user