diff --git a/app/build.gradle b/app/build.gradle
index 5a206becf5..5e5be2e08c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -137,7 +137,7 @@ dependencies {
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha05'
// 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.
implementation "io.coil-kt:coil-compose:$coil_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4316e8668b..b9b88716f6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
index c2c21bb244..f71ef1439b 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
@@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
+import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.ui.note.Nip47URI
import fr.acinq.secp256k1.Hex
import nostr.postr.Persona
@@ -42,6 +43,7 @@ private object PrefKeys {
const val LANGUAGE_PREFS = "languagePreferences"
const val TRANSLATE_TO = "translateTo"
const val ZAP_AMOUNTS = "zapAmounts"
+ const val DEFAULT_ZAPTYPE = "defaultZapType"
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
const val LATEST_CONTACT_LIST = "latestContactList"
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.TRANSLATE_TO, account.translateTo)
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.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
@@ -217,6 +220,11 @@ object LocalPreferences {
object : TypeToken>() {}.type
) ?: listOf(500L, 1000L, 5000L)
+ val defaultZapType = gson.fromJson(
+ getString(PrefKeys.DEFAULT_ZAPTYPE, "PUBLIC"),
+ object : TypeToken() {}.type
+ ) ?: LnZapEvent.ZapType.PUBLIC
+
val zapPaymentRequestServer = try {
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
gson.fromJson(it, Nip47URI::class.java)
@@ -260,6 +268,7 @@ object LocalPreferences {
languagePreferences,
translateTo,
zapAmountChoices,
+ defaultZapType,
zapPaymentRequestServer,
hideDeleteRequestDialog,
hideBlockAlertDialog,
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
index 2ee90218b7..0a7bded58b 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
@@ -44,6 +44,7 @@ class Account(
var languagePreferences: Map = mapOf(),
var translateTo: String = Locale.getDefault().language,
var zapAmountChoices: List = listOf(500L, 1000L, 5000L),
+ var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC,
var zapPaymentRequest: Nip47URI? = null,
var hideDeleteRequestDialog: Boolean = false,
var hideBlockAlertDialog: Boolean = false,
@@ -632,6 +633,12 @@ class Account(
saveable.invalidateData()
}
+ fun changeDefaultZapType(zapType: LnZapEvent.ZapType) {
+ defaultZapType = zapType
+ live.invalidateData()
+ saveable.invalidateData()
+ }
+
fun changeZapAmounts(newAmounts: List) {
zapAmountChoices = newAmounts
live.invalidateData()
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt
index 7e16f77d80..eed0507d54 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt
@@ -84,15 +84,15 @@ fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
} else if (key.startsWith("nprofile", true)) {
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
- return DirtyKeyInfo(pubkeyRelay, restOfWord)
- } else if (key.startsWith("nevent", true)) {
+ return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
+ } else if (key.startsWith("nevent1", true)) {
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
- return DirtyKeyInfo(noteRelayId, restOfWord)
+ return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
} else if (key.startsWith("naddr1", true)) {
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) {
e.printStackTrace()
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
index 4554deb627..1b489bc41c 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
@@ -596,17 +596,6 @@ object LocalCache {
fun consume(event: LnZapEvent) {
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.
if (note.event != null) return
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt
index 7bf4537f7e..0b145008f9 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
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.Relay
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()) {
override fun idNote() = address.toNAddr()
+ override fun toNEvent() = address.toNAddr()
override fun idDisplayNote() = idNote().toShortenHex()
override fun address() = address
override fun createdAt() = (event as? LongTextNoteEvent)?.publishedAt() ?: event?.createdAt()
@@ -51,6 +53,11 @@ open class Note(val idHex: String) {
fun id() = Hex.decode(idHex)
open fun idNote() = id().toNote()
+
+ open fun toNEvent(): String {
+ return Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
+ }
+
open fun idDisplayNote() = idNote().toShortenHex()
fun channelHex(): HexKey? {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt
index 9696a5795a..13a0cdd935 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt
@@ -47,6 +47,7 @@ class User(val pubkeyHex: String) {
fun pubkey() = Hex.decode(pubkeyHex)
fun pubkeyNpub() = pubkey().toNpub()
+
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
override fun toString(): String = pubkeyHex
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt
index a7543e56f0..c4e6ff8ee1 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt
@@ -18,7 +18,7 @@ class FileHeader(
val description: String? = null
) {
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 {
val imageData = URL(fileUrl).readBytes()
val sha256 = MessageDigest.getInstance("SHA-256")
@@ -55,7 +55,7 @@ class FileHeader(
null
}
- onReady(FileHeader(fileUrl, mimeType, hash, size, blurHash, ""))
+ onReady(FileHeader(fileUrl, mimeType, hash, size, blurHash, description))
} catch (e: Exception) {
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
onError()
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt
index 031c2b1cf7..bcd47faeed 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt
@@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.tagSearch
+import com.vitorpamplona.amethyst.service.nip19.Nip19
+import com.vitorpamplona.amethyst.service.nip19.Nip19.nip19regex
open class BaseTextNoteEvent(
id: HexKey,
@@ -31,6 +33,28 @@ open class BaseTextNoteEvent(
} 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
return returningList
}
@@ -51,6 +75,35 @@ open class BaseTextNoteEvent(
} 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
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt
index 9b63fa7991..59f7850e1f 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt
@@ -19,7 +19,7 @@ open class Event(
@SerializedName("created_at") val createdAt: Long,
val kind: Int,
val tags: List>,
- val content: String,
+ var content: String,
val sig: HexKey
) : EventInterface {
override fun id(): HexKey = id
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt
index 87be07093c..6aa8dd754f 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt
@@ -1,8 +1,11 @@
package com.vitorpamplona.amethyst.service.nip19
import android.util.Log
+import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.model.toHexKey
+import nostr.postr.Bech32
import nostr.postr.bechToBytes
+import nostr.postr.toByteArray
import java.util.regex.Pattern
object Nip19 {
@@ -12,7 +15,14 @@ object Nip19 {
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? {
if (uri == null) return null
@@ -26,8 +36,23 @@ object Nip19 {
val uriScheme = matcher.group(1) // nostr:
val type = matcher.group(2) // npub1
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 parsed = when (type.lowercase()) {
"npub1" -> npub(bytes)
@@ -38,12 +63,11 @@ object Nip19 {
"naddr1" -> naddr(bytes)
else -> null
}
- return parsed?.copy(additionalChars = additionalChars)
+ parsed?.copy(additionalChars = additionalChars ?: "")
} 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 {
@@ -79,7 +103,15 @@ object Nip19 {
?.get(0)
?.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? {
@@ -108,8 +140,31 @@ object Nip19 {
val kind = tlv.get(Tlv.Type.KIND.id)
?.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)
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt
index b0d6428f4f..55b5e04e81 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt
@@ -56,19 +56,19 @@ class NewMessageTagger(var channel: Channel?, var mentions: List?, var rep
if (results?.key?.type == Nip19.Type.USER) {
val user = LocalCache.getOrCreateUser(results.key.hex)
- "#[${tagIndex(user)}]${results.restOfWord}"
+ "nostr:${user.pubkeyNpub()}${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.NOTE) {
val note = LocalCache.getOrCreateNote(results.key.hex)
- "#[${tagIndex(note)}]${results.restOfWord}"
+ "nostr:${note.toNEvent()}${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.EVENT) {
val note = LocalCache.getOrCreateNote(results.key.hex)
- "#[${tagIndex(note)}]${results.restOfWord}"
+ "nostr:${note.toNEvent()}${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
- "#[${tagIndex(note)}]${results.restOfWord}"
+ "nostr:${note.idNote()}${results.restOfWord}"
} else {
word
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt
index 35210be770..37904090ff 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt
@@ -1,5 +1,9 @@
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 androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
@@ -13,10 +17,15 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CurrencyBitcoin
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
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.onFocusChanged
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
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.TextFieldValue
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
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.ui.components.*
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.theme.BitcoinOrange
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class)
@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 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 lud16 = user?.info?.lnAddress()
@@ -234,6 +263,14 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
} else {
UrlPreview(myUrlPreview, myUrlPreview)
}
+ } else if (isBechLink(myUrlPreview)) {
+ BechLink(
+ myUrlPreview,
+ true,
+ MaterialTheme.colors.background,
+ accountViewModel,
+ navController
+ )
} else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) {
UrlPreview("https://$myUrlPreview", myUrlPreview)
}
@@ -267,7 +304,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
tint = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(bottom = 10.dp)
) {
- postViewModel.upload(it, context)
+ postViewModel.selectImage(it)
}
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(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)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt
index 5fccd7a432..a7f07605f1 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt
@@ -38,6 +38,9 @@ open class NewPostViewModel : ViewModel() {
var userSuggestions by mutableStateOf>(emptyList())
var userSuggestionAnchor: TextRange? = null
+ // Images and Videos
+ var contentToAddUrl by mutableStateOf(null)
+
// Polls
var canUsePoll by mutableStateOf(false)
var wantsPoll by mutableStateOf(false)
@@ -79,11 +82,12 @@ open class NewPostViewModel : ViewModel() {
}
quote?.let {
- message = TextFieldValue(message.text + "\n\n@${it.idNote()}")
+ message = TextFieldValue(message.text + "\n\n@${it.toNEvent()}")
}
canAddInvoice = account.userProfile().info?.lnAddress() != null
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null
+ contentToAddUrl = null
this.account = account
}
@@ -105,46 +109,15 @@ open class NewPostViewModel : ViewModel() {
cancel()
}
- fun upload(it: Uri, context: Context) {
+ fun upload(it: Uri, description: String, context: Context) {
isUploadingImage = true
+ contentToAddUrl = null
ImageUploader.uploadImage(
uri = it,
contentResolver = context.contentResolver,
onSuccess = { imageUrl, mimeType ->
- 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,
- 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")
- }
- }
- )
- }
+ createNIP97Record(imageUrl, mimeType, description)
},
onError = {
isUploadingImage = false
@@ -157,6 +130,7 @@ open class NewPostViewModel : ViewModel() {
open fun cancel() {
message = TextFieldValue("")
+ contentToAddUrl = null
urlPreview = null
isUploadingImage = false
mentions = null
@@ -220,7 +194,7 @@ open class NewPostViewModel : ViewModel() {
fun canPost(): Boolean {
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) {
@@ -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
+ }
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt
index 1a0715ef29..771b5f6586 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt
@@ -13,9 +13,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
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.NewPostView
@Composable
fun FabColumn(account: Account) {
@@ -88,7 +86,7 @@ fun FabColumn(account: Account) {
}
if (wantsToPost) {
- NewPostView({ wantsToPost = false }, account = NostrAccountDataSource.account)
+ // NewPostView({ wantsToPost = false }, account = NostrAccountDataSource.account)
}
if (wantsToPoll) {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt
index ece9c89e9b..0fe83a465d 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt
@@ -16,18 +16,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.NewPostView
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
-fun NewNoteButton(account: Account) {
+fun NewNoteButton(account: Account, accountViewModel: AccountViewModel, navController: NavController) {
var wantsToPost by remember {
mutableStateOf(false)
}
if (wantsToPost) {
- NewPostView({ wantsToPost = false }, account = account)
+ NewPostView({ wantsToPost = false }, account = account, accountViewModel = accountViewModel, navController = navController)
}
OutlinedButton(
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt
index 06e38c773c..a5c4177283 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt
@@ -37,7 +37,6 @@ import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
-import com.vitorpamplona.amethyst.service.model.LnZapEvent
import kotlinx.coroutines.launch
@Composable
@@ -136,7 +135,7 @@ fun InvoiceRequest(
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
- val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, LnZapEvent.ZapType.PUBLIC)
+ val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType)
LightningAddressResolver().lnAddressInvoice(
lud16,
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt
index ab67d171d7..21b0efb537 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.components
+import android.net.Uri
import android.view.ViewGroup
import android.widget.FrameLayout
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.ExoPlayer
import com.google.android.exoplayer2.MediaItem
+import com.google.android.exoplayer2.MediaMetadata
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
@@ -23,16 +25,33 @@ import com.google.android.exoplayer2.ui.StyledPlayerView
import com.vitorpamplona.amethyst.VideoCache
@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 lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
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 {
repeatMode = Player.REPEAT_MODE_ALL
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
setMediaSource(
- ProgressiveMediaSource.Factory(VideoCache.get()).createMediaSource(MediaItem.fromUri(videoUri))
+ ProgressiveMediaSource.Factory(VideoCache.get()).createMediaSource(
+ media
+ )
)
prepare()
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt
index 69b0535779..8f8c15cfa5 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt
@@ -175,7 +175,7 @@ fun ZoomableContentView(content: ZoomableContent, images: List
}
}
} else {
- VideoView(content.url) { dialogOpen = true }
+ VideoView(content.url, content.description) { dialogOpen = true }
}
if (dialogOpen) {
@@ -322,7 +322,7 @@ private fun RenderImageOrVideo(content: ZoomableContent) {
}
} else {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
- VideoView(content.url)
+ VideoView(content.url, content.description)
}
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt
index 5376499053..5148c06326 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt
@@ -36,8 +36,13 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
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.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.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
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(
authorNotes: Collection,
navController: NavController,
- account: Account
+ account: Account,
+ accountViewModel: AccountViewModel,
+ kind: String = "nonzap"
) {
val accountState by account.userProfile().live().follows.observeAsState()
val accountUser = accountState?.user ?: return
@@ -219,12 +235,39 @@ fun AuthorGallery(
Column(modifier = Modifier.padding(start = 10.dp)) {
FlowRow() {
authorNotes.forEach {
- FastNoteAuthorPicture(
- note = it,
- navController = navController,
- userAccount = accountUser,
- size = 35.dp
- )
+ if (it.event?.content() != "" && kind == "zap") {
+ Row(Modifier.fillMaxWidth()) {
+ FastNoteAuthorPicture(
+ note = it,
+ 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 showFollowingMark = userAccount.isFollowingCached(user) || user === userAccount
-
UserPicture(
userHex = user.pubkeyHex,
userPicture = user.profilePicture(),
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt
index bbcf20e2be..352775a3c0 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt
@@ -450,7 +450,7 @@ fun NoteComposeInner(
} else if (noteEvent is LongTextNoteEvent) {
LongFormHeader(noteEvent, note, loggedIn)
- ReactionsRow(note, accountViewModel)
+ ReactionsRow(note, accountViewModel, navController)
Divider(
modifier = Modifier.padding(top = 10.dp),
@@ -485,7 +485,7 @@ fun NoteComposeInner(
)
}
- ReactionsRow(note, accountViewModel)
+ ReactionsRow(note, accountViewModel, navController)
Divider(
modifier = Modifier.padding(top = 10.dp),
@@ -512,7 +512,7 @@ fun NoteComposeInner(
)
if (!makeItShort) {
- ReactionsRow(note, accountViewModel)
+ ReactionsRow(note, accountViewModel, navController)
}
Divider(
@@ -556,7 +556,7 @@ fun NoteComposeInner(
}
if (!makeItShort) {
- ReactionsRow(note, accountViewModel)
+ ReactionsRow(note, accountViewModel, navController)
}
Divider(
@@ -1196,10 +1196,10 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) {
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))
}
- DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); onDismiss() }) {
+ DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent())); onDismiss() }) {
Text(stringResource(R.string.copy_note_id))
}
DropdownMenuItem(onClick = {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt
index ca98d7048b..e5271bc0f4 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt
@@ -85,7 +85,7 @@ val externalLinkForNote = { note: Note ->
"https://habla.news/a/${note.address().toNAddr()}"
}
} 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)
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)
onDismiss()
}
VerticalDivider(primaryLight)
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)
onDismiss()
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt
index 26e83cf916..28ffb025f5 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt
@@ -250,7 +250,7 @@ fun ZapVote(
zappingProgress = it
}
},
- zapType = LnZapEvent.ZapType.PUBLIC
+ zapType = account.defaultZapType
)
}
} else {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt
index cf9bc98453..f334df0879 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt
@@ -49,12 +49,12 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
+import androidx.navigation.NavController
import coil.compose.AsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.vitorpamplona.amethyst.R
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.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@@ -66,7 +66,7 @@ import java.math.RoundingMode
import kotlin.math.roundToInt
@Composable
-fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
+fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
@@ -79,11 +79,11 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
}
if (wantsToReplyTo != null) {
- NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account)
+ NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, navController)
}
if (wantsToQuote != null) {
- NewPostView({ wantsToQuote = null }, null, wantsToQuote, account)
+ NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, navController)
}
Spacer(modifier = Modifier.height(8.dp))
@@ -364,7 +364,7 @@ fun ZapReaction(
zappingProgress = it
}
},
- zapType = LnZapEvent.ZapType.PUBLIC
+ zapType = account.defaultZapType
)
}
} else if (account.zapAmountChoices.size > 1) {
@@ -561,7 +561,7 @@ fun ZapAmountChoicePopup(
context,
onError,
onProgress,
- LnZapEvent.ZapType.PUBLIC
+ account.defaultZapType
)
onDismiss()
}
@@ -587,7 +587,7 @@ fun ZapAmountChoicePopup(
context,
onError,
onProgress,
- LnZapEvent.ZapType.PUBLIC
+ account.defaultZapType
)
onDismiss()
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt
index 6239a0abbc..7ea074c960 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt
@@ -31,6 +31,7 @@ import kotlinx.coroutines.launch
class ZapOptionstViewModel : ViewModel() {
private var account: Account? = null
+
var customAmount by mutableStateOf(TextFieldValue("21"))
var customMessage by mutableStateOf(TextFieldValue(""))
@@ -73,7 +74,7 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc
)
val zapOptions = zapTypes.map { it.second }
- var selectedZapType by remember { mutableStateOf(zapTypes[0]) }
+ var selectedZapType by remember { mutableStateOf(account.defaultZapType) }
Dialog(
onDismissRequest = { onClose() },
@@ -116,7 +117,7 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc
zappingProgress = it
}
},
- zapType = selectedZapType.first
+ zapType = selectedZapType
)
}
onClose()
@@ -163,7 +164,15 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc
) {
OutlinedTextField(
// 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,
onValueChange = {
postViewModel.customMessage = it
@@ -186,10 +195,11 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc
}
TextSpinner(
label = "Zap Type",
- placeholder = "Public",
+ placeholder = zapTypes.filter { it.first == account.defaultZapType }.first().second,
options = zapOptions,
onSelect = {
- selectedZapType = zapTypes[it]
+ selectedZapType = zapTypes[it].first
+ account.changeDefaultZapType(selectedZapType)
},
modifier = Modifier.fillMaxWidth()
)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt
index 26d0448be4..2b3a6b4663 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt
@@ -378,7 +378,7 @@ fun NoteMaster(
}
}
- ReactionsRow(note, accountViewModel)
+ ReactionsRow(note, accountViewModel, navController)
Divider(
modifier = Modifier.padding(top = 10.dp),
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt
index 784d612629..fe3602a317 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt
@@ -233,7 +233,7 @@ fun ChannelScreen(
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(start = 5.dp)
) {
- channelScreenModel.upload(it, context)
+ channelScreenModel.upload(it, "", context)
}
},
colors = TextFieldDefaults.textFieldColors(
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt
index 07381c5e7c..5dd3bb2ca8 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt
@@ -194,7 +194,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(start = 5.dp)
) {
- chatRoomScreenModel.upload(it, context)
+ chatRoomScreenModel.upload(it, "", context)
}
},
colors = TextFieldDefaults.textFieldColors(
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt
index 49486609d5..0adfec2b03 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt
@@ -73,7 +73,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
}
},
floatingActionButton = {
- FloatingButtons(navController, accountStateViewModel)
+ FloatingButtons(navController, accountViewModel, accountStateViewModel)
},
scaffoldState = scaffoldState
) {
@@ -85,8 +85,8 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
}
@Composable
-fun FloatingButtons(navController: NavHostController, accountViewModel: AccountStateViewModel) {
- val accountState by accountViewModel.accountContent.collectAsState()
+fun FloatingButtons(navController: NavHostController, accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel) {
+ val accountState by accountStateViewModel.accountContent.collectAsState()
if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) {
Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state ->
@@ -98,7 +98,7 @@ fun FloatingButtons(navController: NavHostController, accountViewModel: AccountS
// Does nothing.
}
is AccountState.LoggedIn -> {
- NewNoteButton(state.account)
+ NewNoteButton(state.account, accountViewModel, navController)
}
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3e3109e953..5c0125fb80 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -287,6 +287,9 @@
"Looking for Event %1$s"
Add a public message
+ Add a private message
+ Add an invoice message
+
Thank you for all your work!
Create and Add
@@ -297,4 +300,11 @@
Image is the same since the post
Image has changed. The author might not have seen the change
+ Add Image
+ Add Video
+ Add Document
+ Create and Add
+ Description of the contents
+ A blue boat in a white sandy beach at sunset
+
diff --git a/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt
index 488cb27d22..cbd9950b86 100644
--- a/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt
+++ b/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt
@@ -54,12 +54,24 @@ class NIP19ParserTest {
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
fun nAddrParserGizmo() {
val result = Nip19.uriToRoute("naddr1qpqrvvfnvccrzdryxgunzvtxvgukge34xfjnqdpcv9sk2desxgmrscesvserzd3h8ycrywphvg6nsvf58ycnqef3v5mnsvt98pjnqdfs8ypzq3huhccxt6h34eupz3jeynjgjgek8lel2f4adaea0svyk94a3njdqvzqqqr4gudhrkyk")
assertEquals(Nip19.Type.ADDRESS, result?.type)
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:613f014d2911fb9df52e048aae70268c0d216790287b5814910e1e781e8e0509", result?.hex)
assertEquals(null, result?.relay)
+ assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
+ assertEquals(30023L, result?.kind)
}
@Test
@@ -68,6 +80,8 @@ class NIP19ParserTest {
assertEquals(Nip19.Type.ADDRESS, result?.type)
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418", result?.hex)
assertEquals(null, result?.relay)
+ assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
+ assertEquals(30023L, result?.kind)
}
@Test
@@ -76,5 +90,62 @@ class NIP19ParserTest {
assertEquals(Nip19.Type.EVENT, result?.type)
assertEquals("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", result?.hex)
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)
}
}