Merge branch 'main' of https://github.com/believethehype/amethyst
This commit is contained in:
+4
-2
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId "com.vitorpamplona.amethyst"
|
applicationId "com.vitorpamplona.amethyst"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 103
|
versionCode 105
|
||||||
versionName "0.28.1"
|
versionName "0.29.1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@@ -104,6 +104,8 @@ dependencies {
|
|||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
|
||||||
|
|
||||||
|
implementation "net.engawapg.lib:zoomable:1.4.0"
|
||||||
|
|
||||||
// Biometrics
|
// Biometrics
|
||||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||||
|
|
||||||
|
|||||||
@@ -77,4 +77,13 @@ class TranslationsTest {
|
|||||||
"pt"
|
"pt"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNostrEvents() {
|
||||||
|
assertTranslateContains(
|
||||||
|
"nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy",
|
||||||
|
"sure, nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy",
|
||||||
|
"en"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,10 +357,10 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendPost(message: String, replyTo: List<Note>?, mentions: List<User>?) {
|
fun sendPost(message: String, replyTo: List<Note>?, mentions: List<User>?, tags: List<String>? = null) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
val repliesToHex = replyTo?.map { it.idHex }
|
val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex }
|
||||||
val mentionsHex = mentions?.map { it.pubkeyHex }
|
val mentionsHex = mentions?.map { it.pubkeyHex }
|
||||||
val addresses = replyTo?.mapNotNull { it.address() }
|
val addresses = replyTo?.mapNotNull { it.address() }
|
||||||
|
|
||||||
@@ -369,8 +369,10 @@ class Account(
|
|||||||
replyTos = repliesToHex,
|
replyTos = repliesToHex,
|
||||||
mentions = mentionsHex,
|
mentions = mentionsHex,
|
||||||
addresses = addresses,
|
addresses = addresses,
|
||||||
|
extraTags = tags,
|
||||||
privateKey = loggedIn.privKey!!
|
privateKey = loggedIn.privKey!!
|
||||||
)
|
)
|
||||||
|
|
||||||
Client.send(signedEvent)
|
Client.send(signedEvent)
|
||||||
LocalCache.consume(signedEvent)
|
LocalCache.consume(signedEvent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ open class Note(val idHex: String) {
|
|||||||
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
|
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun address() = (event as? LongTextNoteEvent)?.address()
|
open fun address(): ATag? = null
|
||||||
|
|
||||||
open fun createdAt() = event?.createdAt()
|
open fun createdAt() = event?.createdAt()
|
||||||
|
|
||||||
@@ -235,6 +235,35 @@ open class Note(val idHex: String) {
|
|||||||
}.sumOf { it }
|
}.sumOf { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasPledgeBy(user: User): Boolean {
|
||||||
|
return replies
|
||||||
|
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
|
||||||
|
.any {
|
||||||
|
val pledgeValue = try {
|
||||||
|
BigDecimal(it.event?.content())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
// do nothing if it can't convert to bigdecimal
|
||||||
|
}
|
||||||
|
|
||||||
|
pledgeValue != null && it.author == user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pledgedAmountByOthers(): BigDecimal {
|
||||||
|
return replies
|
||||||
|
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
|
||||||
|
.mapNotNull {
|
||||||
|
try {
|
||||||
|
BigDecimal(it.event?.content())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
// do nothing if it can't convert to bigdecimal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sumOf { it }
|
||||||
|
}
|
||||||
|
|
||||||
fun hasAnyReports(): Boolean {
|
fun hasAnyReports(): Boolean {
|
||||||
val dayAgo = Date().time / 1000 - 24 * 60 * 60
|
val dayAgo = Date().time / 1000 - 24 * 60 * 60
|
||||||
return reports.isNotEmpty() ||
|
return reports.isNotEmpty() ||
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ data class RelaySetupInfo(
|
|||||||
val read: Boolean,
|
val read: Boolean,
|
||||||
val write: Boolean,
|
val write: Boolean,
|
||||||
val errorCount: Int = 0,
|
val errorCount: Int = 0,
|
||||||
val downloadCount: Int = 0,
|
val downloadCountInBytes: Int = 0,
|
||||||
val uploadCount: Int = 0,
|
val uploadCountInBytes: Int = 0,
|
||||||
val spamCount: Int = 0,
|
val spamCount: Int = 0,
|
||||||
val feedTypes: Set<FeedType>
|
val feedTypes: Set<FeedType>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
@@ -42,7 +43,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
|||||||
TypedFilter(
|
TypedFilter(
|
||||||
types = FeedType.values().toSet(),
|
types = FeedType.values().toSet(),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, RepostEvent.kind, LongTextNoteEvent.kind),
|
||||||
authors = listOf(it.pubkeyHex),
|
authors = listOf(it.pubkeyHex),
|
||||||
limit = 200
|
limit = 200
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.lnurl
|
||||||
|
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
object LnWithdrawalUtil {
|
||||||
|
private val withdrawalPattern = Pattern.compile(
|
||||||
|
"lnurl.*",
|
||||||
|
Pattern.CASE_INSENSITIVE
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds LN withdrawal in the provided input string and returns it.
|
||||||
|
* For example for input = "aaa bbb lnbc1xxx ccc" it will return "lnbc1xxx"
|
||||||
|
* It will only return the first withdrawal found in the input.
|
||||||
|
*
|
||||||
|
* @return the invoice if it was found. null for null input or if no invoice is found
|
||||||
|
*/
|
||||||
|
fun findWithdrawal(input: String?): String? {
|
||||||
|
if (input == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val matcher = withdrawalPattern.matcher(input)
|
||||||
|
return if (matcher.find()) {
|
||||||
|
matcher.group()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import fr.acinq.secp256k1.Secp256k1
|
|||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
import java.math.BigDecimal
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ open class Event(
|
|||||||
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hashtags() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
override fun hashtags() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
||||||
|
|
||||||
override fun isTaggedUser(idHex: String) = tags.any { it.getOrNull(0) == "p" && it.getOrNull(1) == idHex }
|
override fun isTaggedUser(idHex: String) = tags.any { it.getOrNull(0) == "p" && it.getOrNull(1) == idHex }
|
||||||
|
|
||||||
@@ -55,6 +56,14 @@ open class Event(
|
|||||||
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags }
|
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags }
|
||||||
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags }?.getOrNull(1)
|
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags }?.getOrNull(1)
|
||||||
|
|
||||||
|
override fun getReward(): BigDecimal? {
|
||||||
|
return try {
|
||||||
|
tags.filter { it.firstOrNull() == "reward" }.mapNotNull { BigDecimal(it.getOrNull(1)) }.firstOrNull()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the ID is correct and then if the pubKey's secret key signed the event.
|
* Checks if the ID is correct and then if the pubKey's secret key signed the event.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
interface EventInterface {
|
interface EventInterface {
|
||||||
fun id(): HexKey
|
fun id(): HexKey
|
||||||
@@ -28,4 +29,7 @@ interface EventInterface {
|
|||||||
fun isTaggedHash(hashtag: String): Boolean
|
fun isTaggedHash(hashtag: String): Boolean
|
||||||
fun isTaggedHashes(hashtag: Set<String>): Boolean
|
fun isTaggedHashes(hashtag: Set<String>): Boolean
|
||||||
fun firstIsTaggedHashes(hashtag: Set<String>): String?
|
fun firstIsTaggedHashes(hashtag: Set<String>): String?
|
||||||
|
fun hashtags(): List<String>
|
||||||
|
|
||||||
|
fun getReward(): BigDecimal?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import com.linkedin.urls.detection.UrlDetector
|
||||||
|
import com.linkedin.urls.detection.UrlDetectorOptions
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.findHashtags
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.findHashtags
|
||||||
@@ -18,7 +20,15 @@ class TextNoteEvent(
|
|||||||
companion object {
|
companion object {
|
||||||
const val kind = 1
|
const val kind = 1
|
||||||
|
|
||||||
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, addresses: List<ATag>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent {
|
fun create(
|
||||||
|
msg: String,
|
||||||
|
replyTos: List<String>?,
|
||||||
|
mentions: List<String>?,
|
||||||
|
addresses: List<ATag>?,
|
||||||
|
extraTags: List<String>?,
|
||||||
|
privateKey: ByteArray,
|
||||||
|
createdAt: Long = Date().time / 1000
|
||||||
|
): TextNoteEvent {
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags = mutableListOf<List<String>>()
|
val tags = mutableListOf<List<String>>()
|
||||||
replyTos?.forEach {
|
replyTos?.forEach {
|
||||||
@@ -33,9 +43,19 @@ class TextNoteEvent(
|
|||||||
findHashtags(msg).forEach {
|
findHashtags(msg).forEach {
|
||||||
tags.add(listOf("t", it))
|
tags.add(listOf("t", it))
|
||||||
}
|
}
|
||||||
|
extraTags?.forEach {
|
||||||
|
tags.add(listOf("t", it))
|
||||||
|
}
|
||||||
|
findURLs(msg).forEach {
|
||||||
|
tags.add(listOf("r", it))
|
||||||
|
}
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||||
val sig = Utils.sign(id, privateKey)
|
val sig = Utils.sign(id, privateKey)
|
||||||
return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
|
return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findURLs(text: String): List<String> {
|
||||||
|
return UrlDetector(text, UrlDetectorOptions.Default).detect().map { it.originalUrl }
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.regex.Pattern
|
|||||||
|
|
||||||
object Nip19 {
|
object Nip19 {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
USER, NOTE, RELAY, ADDRESS
|
USER, NOTE, EVENT, RELAY, ADDRESS
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -76,7 +76,7 @@ object Nip19 {
|
|||||||
?.get(0)
|
?.get(0)
|
||||||
?.toString(Charsets.UTF_8)
|
?.toString(Charsets.UTF_8)
|
||||||
|
|
||||||
return Return(Type.USER, hex, relay)
|
return Return(Type.EVENT, hex, relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun nrelay(bytes: ByteArray): Return? {
|
private fun nrelay(bytes: ByteArray): Return? {
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ class Relay(
|
|||||||
private var socket: WebSocket? = null
|
private var socket: WebSocket? = null
|
||||||
private var isReady: Boolean = false
|
private var isReady: Boolean = false
|
||||||
|
|
||||||
var eventDownloadCounter = 0
|
var eventDownloadCounterInBytes = 0
|
||||||
|
var eventUploadCounterInBytes = 0
|
||||||
|
|
||||||
var spamCounter = 0
|
var spamCounter = 0
|
||||||
var eventUploadCounter = 0
|
|
||||||
var errorCounter = 0
|
var errorCounter = 0
|
||||||
var ping: Long? = null
|
var ping: Long? = null
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ class Relay(
|
|||||||
when (type) {
|
when (type) {
|
||||||
"EVENT" -> {
|
"EVENT" -> {
|
||||||
// Log.w("Relay", "Relay onEVENT $url, $channel")
|
// Log.w("Relay", "Relay onEVENT $url, $channel")
|
||||||
eventDownloadCounter++
|
eventDownloadCounterInBytes += text.bytesUsedInMemory()
|
||||||
val event = Event.fromJson(msg[2], Client.lenient)
|
val event = Event.fromJson(msg[2], Client.lenient)
|
||||||
listeners.forEach { it.onEvent(this@Relay, channel, event) }
|
listeners.forEach { it.onEvent(this@Relay, channel, event) }
|
||||||
}
|
}
|
||||||
@@ -185,6 +186,7 @@ class Relay(
|
|||||||
"""["REQ","$requestId",${filters.take(10).joinToString(",") { it.filter.toJson() }}]"""
|
"""["REQ","$requestId",${filters.take(10).joinToString(",") { it.filter.toJson() }}]"""
|
||||||
// println("FILTERSSENT ${url} ${request}")
|
// println("FILTERSSENT ${url} ${request}")
|
||||||
socket?.send(request)
|
socket?.send(request)
|
||||||
|
eventUploadCounterInBytes += request.bytesUsedInMemory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -209,8 +211,9 @@ class Relay(
|
|||||||
|
|
||||||
fun send(signedEvent: EventInterface) {
|
fun send(signedEvent: EventInterface) {
|
||||||
if (write) {
|
if (write) {
|
||||||
socket?.send("""["EVENT",${signedEvent.toJson()}]""")
|
val event = """["EVENT",${signedEvent.toJson()}]"""
|
||||||
eventUploadCounter++
|
socket?.send(event)
|
||||||
|
eventUploadCounterInBytes += event.bytesUsedInMemory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,3 +260,7 @@ class Relay(
|
|||||||
fun onRelayStateChange(relay: Relay, type: Type, channel: String?)
|
fun onRelayStateChange(relay: Relay, type: Type, channel: String?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.bytesUsedInMemory(): Int {
|
||||||
|
return (8 * ((((this.length) * 2) + 45) / 8))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.vitorpamplona.amethyst.ui
|
package com.vitorpamplona.amethyst.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
@@ -34,6 +37,8 @@ class MainActivity : FragmentActivity() {
|
|||||||
val startingPage = when (nip19?.type) {
|
val startingPage = when (nip19?.type) {
|
||||||
Nip19.Type.USER -> "User/${nip19.hex}"
|
Nip19.Type.USER -> "User/${nip19.hex}"
|
||||||
Nip19.Type.NOTE -> "Note/${nip19.hex}"
|
Nip19.Type.NOTE -> "Note/${nip19.hex}"
|
||||||
|
Nip19.Type.EVENT -> "Event/${nip19.hex}"
|
||||||
|
Nip19.Type.ADDRESS -> "Note/${nip19.hex}"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,3 +99,13 @@ class MainActivity : FragmentActivity() {
|
|||||||
ServiceManager.cleanUp()
|
ServiceManager.cleanUp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GetMediaActivityResultContract : ActivityResultContracts.GetContent() {
|
||||||
|
|
||||||
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
|
return super.createIntent(context, input).apply {
|
||||||
|
// Force only images and videos to be selectable
|
||||||
|
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,13 +19,16 @@ object ImageUploader {
|
|||||||
onError: (Throwable) -> Unit
|
onError: (Throwable) -> Unit
|
||||||
) {
|
) {
|
||||||
val contentType = contentResolver.getType(uri)
|
val contentType = contentResolver.getType(uri)
|
||||||
|
val category = contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
||||||
|
|
||||||
|
val url = if (category == "image") "https://api.imgur.com/3/image" else "https://api.imgur.com/3/upload"
|
||||||
|
|
||||||
val client = OkHttpClient.Builder().build()
|
val client = OkHttpClient.Builder().build()
|
||||||
|
|
||||||
val requestBody: RequestBody = MultipartBody.Builder()
|
val requestBody: RequestBody = MultipartBody.Builder()
|
||||||
.setType(MultipartBody.FORM)
|
.setType(MultipartBody.FORM)
|
||||||
.addFormDataPart(
|
.addFormDataPart(
|
||||||
"image",
|
category,
|
||||||
"${UUID.randomUUID()}",
|
"${UUID.randomUUID()}",
|
||||||
object : RequestBody() {
|
object : RequestBody() {
|
||||||
override fun contentType(): MediaType? =
|
override fun contentType(): MediaType? =
|
||||||
@@ -46,7 +49,7 @@ object ImageUploader {
|
|||||||
val request: Request = Request.Builder()
|
val request: Request = Request.Builder()
|
||||||
.header("Authorization", "Client-ID e6aea87296f3f96")
|
.header("Authorization", "Client-ID e6aea87296f3f96")
|
||||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
.url("https://api.imgur.com/3/image")
|
.url(url)
|
||||||
.post(requestBody)
|
.post(requestBody)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
|||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
import com.vitorpamplona.amethyst.ui.components.isValidURL
|
import com.vitorpamplona.amethyst.ui.components.isValidURL
|
||||||
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
|
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -85,6 +87,8 @@ class NewPostViewModel : ViewModel() {
|
|||||||
addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
|
addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
|
||||||
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
||||||
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
|
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
|
||||||
|
} else if (results?.key?.type == Nip19.Type.EVENT) {
|
||||||
|
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
|
||||||
} 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) {
|
||||||
@@ -105,6 +109,10 @@ class NewPostViewModel : ViewModel() {
|
|||||||
} 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}"
|
||||||
|
} else if (results?.key?.type == Nip19.Type.EVENT) {
|
||||||
|
val note = LocalCache.getOrCreateNote(results.key.hex)
|
||||||
|
|
||||||
"#[${tagIndex(note)}]${results.restOfWord}"
|
"#[${tagIndex(note)}]${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)
|
||||||
@@ -140,12 +148,16 @@ class NewPostViewModel : ViewModel() {
|
|||||||
onSuccess = { imageUrl ->
|
onSuccess = { imageUrl ->
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
message = TextFieldValue(message.text + "\n\n" + imageUrl)
|
message = TextFieldValue(message.text + "\n\n" + imageUrl)
|
||||||
urlPreview = findUrlInMessage()
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
delay(2000)
|
||||||
|
urlPreview = findUrlInMessage()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
imageUploadingError.emit("Failed to upload the image")
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -150,27 +150,27 @@ fun ServerConfigHeader() {
|
|||||||
|
|
||||||
Column(Modifier.weight(1.4f)) {
|
Column(Modifier.weight(1.4f)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Spacer(modifier = Modifier.size(25.dp))
|
Spacer(modifier = Modifier.size(30.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.posts),
|
text = stringResource(R.string.bytes),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1.2f),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.size(10.dp))
|
Spacer(modifier = Modifier.size(5.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.posts),
|
text = stringResource(id = R.string.bytes),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1.2f),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.size(10.dp))
|
Spacer(modifier = Modifier.size(5.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.errors),
|
text = stringResource(R.string.errors),
|
||||||
@@ -371,10 +371,10 @@ fun ServerConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${countToHumanReadable(item.downloadCount)}",
|
text = "${countToHumanReadable(item.downloadCountInBytes)}",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = 14.sp,
|
fontSize = 12.sp,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1.2f),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -399,10 +399,10 @@ fun ServerConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${countToHumanReadable(item.uploadCount)}",
|
text = "${countToHumanReadable(item.uploadCountInBytes)}",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = 14.sp,
|
fontSize = 12.sp,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1.2f),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -418,7 +418,7 @@ fun ServerConfig(
|
|||||||
Text(
|
Text(
|
||||||
text = "${countToHumanReadable(item.errorCount)}",
|
text = "${countToHumanReadable(item.errorCount)}",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = 14.sp,
|
fontSize = 12.sp,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
)
|
)
|
||||||
@@ -433,7 +433,7 @@ fun ServerConfig(
|
|||||||
Text(
|
Text(
|
||||||
text = "${countToHumanReadable(item.spamCount)}",
|
text = "${countToHumanReadable(item.spamCount)}",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = 14.sp,
|
fontSize = 12.sp,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,23 +49,23 @@ class NewRelayListViewModel : ViewModel() {
|
|||||||
val localInfoFeedTypes = account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes ?: FeedType.values().toSet()
|
val localInfoFeedTypes = account.localRelays.filter { localRelay -> localRelay.url == it.key }.firstOrNull()?.feedTypes ?: FeedType.values().toSet()
|
||||||
|
|
||||||
val errorCounter = liveRelay?.errorCounter ?: 0
|
val errorCounter = liveRelay?.errorCounter ?: 0
|
||||||
val eventDownloadCounter = liveRelay?.eventDownloadCounter ?: 0
|
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
|
||||||
val eventUploadCounter = liveRelay?.eventUploadCounter ?: 0
|
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
|
||||||
val spamCounter = liveRelay?.spamCounter ?: 0
|
val spamCounter = liveRelay?.spamCounter ?: 0
|
||||||
|
|
||||||
RelaySetupInfo(it.key, it.value.read, it.value.write, errorCounter, eventDownloadCounter, eventUploadCounter, spamCounter, localInfoFeedTypes)
|
RelaySetupInfo(it.key, it.value.read, it.value.write, errorCounter, eventDownloadCounter, eventUploadCounter, spamCounter, localInfoFeedTypes)
|
||||||
}.sortedBy { it.downloadCount }.reversed()
|
}.sortedBy { it.downloadCountInBytes }.reversed()
|
||||||
} else {
|
} else {
|
||||||
account.localRelays.map {
|
account.localRelays.map {
|
||||||
val liveRelay = RelayPool.getRelay(it.url)
|
val liveRelay = RelayPool.getRelay(it.url)
|
||||||
|
|
||||||
val errorCounter = liveRelay?.errorCounter ?: 0
|
val errorCounter = liveRelay?.errorCounter ?: 0
|
||||||
val eventDownloadCounter = liveRelay?.eventDownloadCounter ?: 0
|
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
|
||||||
val eventUploadCounter = liveRelay?.eventUploadCounter ?: 0
|
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
|
||||||
val spamCounter = liveRelay?.spamCounter ?: 0
|
val spamCounter = liveRelay?.spamCounter ?: 0
|
||||||
|
|
||||||
RelaySetupInfo(it.url, it.read, it.write, errorCounter, eventDownloadCounter, eventUploadCounter, spamCounter, it.feedTypes)
|
RelaySetupInfo(it.url, it.read, it.write, errorCounter, eventDownloadCounter, eventUploadCounter, spamCounter, it.feedTypes)
|
||||||
}.sortedBy { it.downloadCount }.reversed()
|
}.sortedBy { it.downloadCountInBytes }.reversed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.ui.actions
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.infiniteRepeatable
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
@@ -28,6 +27,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.GetMediaActivityResultContract
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@@ -150,7 +150,7 @@ fun GallerySelect(
|
|||||||
) {
|
) {
|
||||||
var hasLaunched by remember { mutableStateOf(AtomicBoolean(false)) }
|
var hasLaunched by remember { mutableStateOf(AtomicBoolean(false)) }
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.GetContent(),
|
contract = GetMediaActivityResultContract(),
|
||||||
onResult = { uri: Uri? ->
|
onResult = { uri: Uri? ->
|
||||||
onImageUri(uri)
|
onImageUri(uri)
|
||||||
hasLaunched.set(false)
|
hasLaunched.set(false)
|
||||||
@@ -161,7 +161,7 @@ fun GallerySelect(
|
|||||||
fun LaunchGallery() {
|
fun LaunchGallery() {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
if (!hasLaunched.getAndSet(true)) {
|
if (!hasLaunched.getAndSet(true)) {
|
||||||
launcher.launch("image/*")
|
launcher.launch("*/*")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.ui.text.withStyle
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -47,11 +48,28 @@ fun ClickableRoute(
|
|||||||
|
|
||||||
if (note.event is ChannelCreateEvent) {
|
if (note.event is ChannelCreateEvent) {
|
||||||
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Channel/${nip19.hex}", navController)
|
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Channel/${nip19.hex}", navController)
|
||||||
|
} else if (note.event is PrivateDmEvent) {
|
||||||
|
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Room/${note.author?.pubkeyHex}", navController)
|
||||||
} else if (channel != null) {
|
} else if (channel != null) {
|
||||||
CreateClickableText(channel.toBestDisplayName(), nip19.additionalChars, "Channel/${note.channel()?.idHex}", navController)
|
CreateClickableText(channel.toBestDisplayName(), nip19.additionalChars, "Channel/${note.channel()?.idHex}", navController)
|
||||||
} else {
|
} else {
|
||||||
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Note/${nip19.hex}", navController)
|
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Note/${nip19.hex}", navController)
|
||||||
}
|
}
|
||||||
|
} else if (nip19.type == Nip19.Type.EVENT) {
|
||||||
|
val noteBase = LocalCache.getOrCreateNote(nip19.hex)
|
||||||
|
val noteState by noteBase.live().metadata.observeAsState()
|
||||||
|
val note = noteState?.note ?: return
|
||||||
|
val channel = note.channel()
|
||||||
|
|
||||||
|
if (note.event is ChannelCreateEvent) {
|
||||||
|
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Channel/${nip19.hex}", navController)
|
||||||
|
} else if (note.event is PrivateDmEvent) {
|
||||||
|
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Room/${note.author?.pubkeyHex}", navController)
|
||||||
|
} else if (channel != null) {
|
||||||
|
CreateClickableText(channel.toBestDisplayName(), nip19.additionalChars, "Channel/${note.channel()?.idHex}", navController)
|
||||||
|
} else {
|
||||||
|
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Event/${nip19.hex}", navController)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
"@${nip19.hex}${nip19.additionalChars} "
|
"@${nip19.hex}${nip19.additionalChars} "
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.material.LocalTextStyle
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClickableWithdrawal(withdrawalString: String) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
ClickableText(
|
||||||
|
text = AnnotatedString("$withdrawalString "),
|
||||||
|
onClick = {
|
||||||
|
runCatching {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$withdrawalString"))
|
||||||
|
ContextCompat.startActivity(context, intent, null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ import com.halilibo.richtext.ui.resolveDefaults
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
|
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
|
||||||
|
import com.vitorpamplona.amethyst.service.lnurl.LnWithdrawalUtil
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -68,6 +69,8 @@ fun isValidURL(url: String?): Boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val richTextDefaults = RichTextStyle().resolveDefaults()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RichTextViewer(
|
fun RichTextViewer(
|
||||||
content: String,
|
content: String,
|
||||||
@@ -78,8 +81,8 @@ fun RichTextViewer(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
val myMarkDownStyle = RichTextStyle().resolveDefaults().copy(
|
val myMarkDownStyle = richTextDefaults.copy(
|
||||||
codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy(
|
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp
|
||||||
@@ -99,7 +102,7 @@ fun RichTextViewer(
|
|||||||
.compositeOver(backgroundColor)
|
.compositeOver(backgroundColor)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
stringStyle = RichTextStyle().resolveDefaults().stringStyle?.copy(
|
stringStyle = richTextDefaults.stringStyle?.copy(
|
||||||
linkStyle = SpanStyle(
|
linkStyle = SpanStyle(
|
||||||
textDecoration = TextDecoration.Underline,
|
textDecoration = TextDecoration.Underline,
|
||||||
color = MaterialTheme.colors.primary
|
color = MaterialTheme.colors.primary
|
||||||
@@ -128,6 +131,20 @@ fun RichTextViewer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
val imagesForPager = mutableListOf<String>()
|
||||||
|
|
||||||
|
content.split('\n').forEach { paragraph ->
|
||||||
|
paragraph.split(' ').forEach { word: String ->
|
||||||
|
// sequence of images will render in a slideview
|
||||||
|
if (isValidURL(word)) {
|
||||||
|
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
||||||
|
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
|
imagesForPager.add(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||||
content.split('\n').forEach { paragraph ->
|
content.split('\n').forEach { paragraph ->
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
@@ -136,17 +153,21 @@ fun RichTextViewer(
|
|||||||
if (canPreview) {
|
if (canPreview) {
|
||||||
// Explicit URL
|
// Explicit URL
|
||||||
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
||||||
if (lnInvoice != null) {
|
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
|
||||||
InvoicePreview(lnInvoice)
|
|
||||||
} else if (isValidURL(word)) {
|
if (isValidURL(word)) {
|
||||||
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
||||||
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
ZoomableImageView(word)
|
ZoomableImageView(word, imagesForPager)
|
||||||
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
VideoView(word)
|
VideoView(word)
|
||||||
} else {
|
} else {
|
||||||
UrlPreview(word, word)
|
UrlPreview(word, "$word ")
|
||||||
}
|
}
|
||||||
|
} else if (lnInvoice != null) {
|
||||||
|
InvoicePreview(lnInvoice)
|
||||||
|
} else if (lnWithdrawal != null) {
|
||||||
|
ClickableWithdrawal(withdrawalString = lnWithdrawal)
|
||||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||||
ClickableEmail(word)
|
ClickableEmail(word)
|
||||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
|
import com.google.accompanist.pager.PagerState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SlidingCarousel(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
pagerState: PagerState = remember { PagerState() },
|
||||||
|
itemsCount: Int,
|
||||||
|
itemContent: @Composable (index: Int) -> Unit
|
||||||
|
) {
|
||||||
|
val isDragged by pagerState.interactionSource.collectIsDraggedAsState()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
HorizontalPager(count = itemsCount, state = pagerState) { page ->
|
||||||
|
itemContent(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// you can remove the surface in case you don't want
|
||||||
|
// the transparant bacground
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 8.dp)
|
||||||
|
.align(Alignment.BottomCenter),
|
||||||
|
shape = CircleShape,
|
||||||
|
color = Color.Black.copy(alpha = 0.5f)
|
||||||
|
) {
|
||||||
|
DotsIndicator(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
|
totalDots = itemsCount,
|
||||||
|
selectedIndex = if (isDragged) pagerState.currentPage else pagerState.targetPage,
|
||||||
|
dotSize = 8.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DotsIndicator(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
totalDots: Int,
|
||||||
|
selectedIndex: Int,
|
||||||
|
selectedColor: Color = MaterialTheme.colors.primary /* Color.Yellow */,
|
||||||
|
unSelectedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) /* Color.Gray */,
|
||||||
|
dotSize: Dp
|
||||||
|
) {
|
||||||
|
LazyRow(
|
||||||
|
modifier = modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
) {
|
||||||
|
items(totalDots) { index ->
|
||||||
|
IndicatorDot(
|
||||||
|
color = if (index == selectedIndex) selectedColor else unSelectedColor,
|
||||||
|
size = dotSize
|
||||||
|
)
|
||||||
|
|
||||||
|
if (index != totalDots - 1) {
|
||||||
|
Spacer(modifier = Modifier.padding(horizontal = 2.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IndicatorDot(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp,
|
||||||
|
color: Color
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(color)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
|
||||||
import androidx.compose.foundation.gestures.calculatePan
|
|
||||||
import androidx.compose.foundation.gestures.calculateZoom
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import com.vitorpamplona.amethyst.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ZoomableAsyncImage(imageUrl: String) {
|
|
||||||
var scale by remember { mutableStateOf(1f) }
|
|
||||||
var offsetX by remember { mutableStateOf(0f) }
|
|
||||||
var offsetY by remember { mutableStateOf(0f) }
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
awaitEachGesture {
|
|
||||||
awaitFirstDown()
|
|
||||||
do {
|
|
||||||
val event = awaitPointerEvent()
|
|
||||||
scale *= event.calculateZoom()
|
|
||||||
val offset = event.calculatePan()
|
|
||||||
offsetX += offset.x
|
|
||||||
offsetY += offset.y
|
|
||||||
} while (event.changes.any { it.pressed })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = imageUrl,
|
|
||||||
contentDescription = stringResource(id = R.string.profile_image),
|
|
||||||
contentScale = ContentScale.FillWidth,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.graphicsLayer(
|
|
||||||
scaleX = scale,
|
|
||||||
scaleY = scale,
|
|
||||||
translationX = offsetX,
|
|
||||||
translationY = offsetY
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,22 +18,27 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
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 coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.PagerState
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
|
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
|
||||||
|
import net.engawapg.lib.zoomable.rememberZoomState
|
||||||
|
import net.engawapg.lib.zoomable.zoomable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
fun ZoomableImageView(word: String) {
|
fun ZoomableImageView(word: String, images: List<String> = listOf(word)) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
// store the dialog open or close state
|
// store the dialog open or close state
|
||||||
@@ -49,7 +54,11 @@ fun ZoomableImageView(word: String) {
|
|||||||
.padding(top = 4.dp)
|
.padding(top = 4.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(shape = RoundedCornerShape(15.dp))
|
.clip(shape = RoundedCornerShape(15.dp))
|
||||||
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp))
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||||
|
RoundedCornerShape(15.dp)
|
||||||
|
)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = { dialogOpen = true },
|
onClick = { dialogOpen = true },
|
||||||
onLongClick = { clipboardManager.setText(AnnotatedString(word)) }
|
onLongClick = { clipboardManager.setText(AnnotatedString(word)) }
|
||||||
@@ -57,12 +66,13 @@ fun ZoomableImageView(word: String) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
ZoomableImageDialog(word, onDismiss = { dialogOpen = false })
|
ZoomableImageDialog(word, images, onDismiss = { dialogOpen = false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ZoomableImageDialog(imageUrl: String, onDismiss: () -> Unit) {
|
fun ZoomableImageDialog(imageUrl: String, allImages: List<String> = listOf(imageUrl), onDismiss: () -> Unit) {
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
@@ -71,6 +81,8 @@ fun ZoomableImageDialog(imageUrl: String, onDismiss: () -> Unit) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp)
|
||||||
) {
|
) {
|
||||||
|
var pagerState: PagerState = remember { PagerState() }
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
@@ -79,10 +91,34 @@ fun ZoomableImageDialog(imageUrl: String, onDismiss: () -> Unit) {
|
|||||||
) {
|
) {
|
||||||
CloseButton(onCancel = onDismiss)
|
CloseButton(onCancel = onDismiss)
|
||||||
|
|
||||||
SaveToGallery(url = imageUrl)
|
SaveToGallery(url = allImages[pagerState.currentPage])
|
||||||
}
|
}
|
||||||
|
|
||||||
ZoomableAsyncImage(imageUrl)
|
if (allImages.size > 1) {
|
||||||
|
SlidingCarousel(
|
||||||
|
pagerState = pagerState,
|
||||||
|
itemsCount = allImages.size,
|
||||||
|
itemContent = { index ->
|
||||||
|
AsyncImage(
|
||||||
|
model = allImages[index],
|
||||||
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zoomable(rememberZoomState())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AsyncImage(
|
||||||
|
model = imageUrl,
|
||||||
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zoomable(rememberZoomState())
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.ui.dal
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
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.service.model.RepostEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object HomeConversationsFeedFilter : FeedFilter<Note>() {
|
object HomeConversationsFeedFilter : FeedFilter<Note>() {
|
||||||
@@ -16,7 +15,7 @@ object HomeConversationsFeedFilter : FeedFilter<Note>() {
|
|||||||
|
|
||||||
return LocalCache.notes.values
|
return LocalCache.notes.values
|
||||||
.filter {
|
.filter {
|
||||||
(it.event is TextNoteEvent || it.event is RepostEvent) &&
|
(it.event is TextNoteEvent) &&
|
||||||
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
||||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||||
it.author?.let { !account.isHidden(it) } ?: true &&
|
it.author?.let { !account.isHidden(it) } ?: true &&
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ object HomeNewThreadFeedFilter : FeedFilter<Note>() {
|
|||||||
|
|
||||||
val notes = LocalCache.notes.values
|
val notes = LocalCache.notes.values
|
||||||
.filter { it ->
|
.filter { it ->
|
||||||
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent) &&
|
(it.event is TextNoteEvent || it.event is RepostEvent) &&
|
||||||
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
||||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||||
it.author?.let { !account.isHidden(it) } ?: true &&
|
it.author?.let { !account.isHidden(it) } ?: true &&
|
||||||
@@ -26,7 +26,7 @@ object HomeNewThreadFeedFilter : FeedFilter<Note>() {
|
|||||||
|
|
||||||
val longFormNotes = LocalCache.addressables.values
|
val longFormNotes = LocalCache.addressables.values
|
||||||
.filter { it ->
|
.filter { it ->
|
||||||
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent) &&
|
(it.event is LongTextNoteEvent) &&
|
||||||
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
||||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||||
it.author?.let { !account.isHidden(it) } ?: true &&
|
it.author?.let { !account.isHidden(it) } ?: true &&
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
|
||||||
@@ -126,6 +127,16 @@ fun AppNavigation(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Route.Event.let { route ->
|
||||||
|
composable(route.route, route.arguments, content = {
|
||||||
|
LoadRedirectScreen(
|
||||||
|
eventId = it.arguments?.getString("id"),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextPage != null) {
|
if (nextPage != null) {
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ sealed class Route(
|
|||||||
icon = R.drawable.ic_moments,
|
icon = R.drawable.ic_moments,
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object Event : Route(
|
||||||
|
route = "Event/{id}",
|
||||||
|
icon = R.drawable.ic_moments,
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// **
|
// **
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||||
|
|
||||||
|
class AddBountyAmountViewModel : ViewModel() {
|
||||||
|
private var account: Account? = null
|
||||||
|
private var bounty: Note? = null
|
||||||
|
|
||||||
|
var nextAmount by mutableStateOf(TextFieldValue(""))
|
||||||
|
|
||||||
|
fun load(account: Account, bounty: Note?) {
|
||||||
|
this.account = account
|
||||||
|
this.bounty = bounty
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendPost() {
|
||||||
|
val newValue = nextAmount.text.trim().toLongOrNull()
|
||||||
|
|
||||||
|
if (newValue != null) {
|
||||||
|
account?.sendPost(
|
||||||
|
newValue.toString(),
|
||||||
|
listOfNotNull(bounty),
|
||||||
|
listOfNotNull(bounty?.author),
|
||||||
|
listOf("bounty-added-reward")
|
||||||
|
)
|
||||||
|
|
||||||
|
nextAmount = TextFieldValue("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
nextAmount = TextFieldValue("")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasChanged(): Boolean {
|
||||||
|
return nextAmount.text.trim().toLongOrNull() != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AddBountyAmountDialog(bounty: Note, account: Account, onClose: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val postViewModel: AddBountyAmountViewModel = viewModel()
|
||||||
|
|
||||||
|
LaunchedEffect(account) {
|
||||||
|
postViewModel.load(account, bounty)
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = { onClose() },
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnClickOutside = false,
|
||||||
|
usePlatformDefaultWidth = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Surface() {
|
||||||
|
Column(modifier = Modifier.padding(10.dp).width(IntrinsicSize.Min)) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
CloseButton(onCancel = {
|
||||||
|
postViewModel.cancel()
|
||||||
|
onClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
PostButton(
|
||||||
|
onPost = {
|
||||||
|
postViewModel.sendPost()
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
isActive = postViewModel.hasChanged()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringResource(R.string.pledge_amount_in_sats)) },
|
||||||
|
value = postViewModel.nextAmount,
|
||||||
|
onValueChange = {
|
||||||
|
postViewModel.nextAmount = it
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
keyboardType = KeyboardType.Number
|
||||||
|
),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "10000, 50000, 5000000",
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -365,7 +365,7 @@ private fun RelayBadges(baseNote: Note) {
|
|||||||
|
|
||||||
FlowRow(Modifier.padding(start = 10.dp)) {
|
FlowRow(Modifier.padding(start = 10.dp)) {
|
||||||
relaysToDisplay.forEach {
|
relaysToDisplay.forEach {
|
||||||
val url = it.removePrefix("wss://")
|
val url = it.removePrefix("wss://").removePrefix("ws://")
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.size(15.dp)
|
.size(15.dp)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -76,18 +77,18 @@ fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): State<Bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ObserveDisplayNip05Status(baseNote: Note) {
|
fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifier) {
|
||||||
val noteState by baseNote.live().metadata.observeAsState()
|
val noteState by baseNote.live().metadata.observeAsState()
|
||||||
val note = noteState?.note ?: return
|
val note = noteState?.note ?: return
|
||||||
|
|
||||||
val author = note.author
|
val author = note.author
|
||||||
if (author != null) {
|
if (author != null) {
|
||||||
ObserveDisplayNip05Status(author)
|
ObserveDisplayNip05Status(author, columnModifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ObserveDisplayNip05Status(baseUser: User) {
|
fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifier) {
|
||||||
val userState by baseUser.live().metadata.observeAsState()
|
val userState by baseUser.live().metadata.observeAsState()
|
||||||
val user = userState?.user ?: return
|
val user = userState?.user ?: return
|
||||||
|
|
||||||
@@ -96,52 +97,54 @@ fun ObserveDisplayNip05Status(baseUser: User) {
|
|||||||
user.nip05()?.let { nip05 ->
|
user.nip05()?.let { nip05 ->
|
||||||
if (nip05.split("@").size == 2) {
|
if (nip05.split("@").size == 2) {
|
||||||
val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex)
|
val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex)
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Column(modifier = columnModifier) {
|
||||||
if (nip05.split("@")[0] != "_") {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
if (nip05.split("@")[0] != "_") {
|
||||||
text = AnnotatedString(nip05.split("@")[0]),
|
Text(
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
text = AnnotatedString(nip05.split("@")[0]),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nip05Verified == null) {
|
||||||
|
Icon(
|
||||||
|
tint = Color.Yellow,
|
||||||
|
imageVector = Icons.Default.Downloading,
|
||||||
|
contentDescription = "Downloading",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(14.dp)
|
||||||
|
.padding(top = 1.dp)
|
||||||
|
)
|
||||||
|
} else if (nip05Verified == true) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_verified),
|
||||||
|
"NIP-05 Verified",
|
||||||
|
tint = Nip05.copy(0.52f),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(14.dp)
|
||||||
|
.padding(top = 1.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
tint = Color.Red,
|
||||||
|
imageVector = Icons.Default.Report,
|
||||||
|
contentDescription = "Invalid Nip05",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(14.dp)
|
||||||
|
.padding(top = 1.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableText(
|
||||||
|
text = AnnotatedString(nip05.split("@")[1]),
|
||||||
|
onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } },
|
||||||
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Visible
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nip05Verified == null) {
|
|
||||||
Icon(
|
|
||||||
tint = Color.Yellow,
|
|
||||||
imageVector = Icons.Default.Downloading,
|
|
||||||
contentDescription = "Downloading",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(14.dp)
|
|
||||||
.padding(top = 1.dp)
|
|
||||||
)
|
|
||||||
} else if (nip05Verified == true) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_verified),
|
|
||||||
"NIP-05 Verified",
|
|
||||||
tint = Nip05.copy(0.52f),
|
|
||||||
modifier = Modifier
|
|
||||||
.size(14.dp)
|
|
||||||
.padding(top = 1.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
tint = Color.Red,
|
|
||||||
imageVector = Icons.Default.Report,
|
|
||||||
contentDescription = "Invalid Nip05",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(14.dp)
|
|
||||||
.padding(top = 1.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ClickableText(
|
|
||||||
text = AnnotatedString(nip05.split("@")[1]),
|
|
||||||
onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } },
|
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Visible
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.foundation.text.ClickableText
|
||||||
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.Bolt
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.outlined.Bolt
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -42,6 +44,7 @@ import coil.compose.AsyncImage
|
|||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.vitorpamplona.amethyst.NotificationCache
|
import com.vitorpamplona.amethyst.NotificationCache
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
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
|
||||||
@@ -50,6 +53,7 @@ import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.EventInterface
|
||||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||||
@@ -65,9 +69,11 @@ import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Following
|
import com.vitorpamplona.amethyst.ui.theme.Following
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -273,15 +279,8 @@ fun NoteCompose(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
)
|
)
|
||||||
}
|
} else {
|
||||||
|
DisplayFollowingHashtagsInPost(noteEvent, account, navController)
|
||||||
val firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet())
|
|
||||||
if (firstTag != null) {
|
|
||||||
ClickableText(
|
|
||||||
text = AnnotatedString(" #$firstTag"),
|
|
||||||
onClick = { navController.navigate("Hashtag/$firstTag") },
|
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -306,7 +305,14 @@ fun NoteCompose(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (note.author != null && !makeItShort && !isQuotedNote) {
|
if (note.author != null && !makeItShort && !isQuotedNote) {
|
||||||
ObserveDisplayNip05Status(note.author!!)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
ObserveDisplayNip05Status(note.author!!, Modifier.weight(1f))
|
||||||
|
|
||||||
|
val baseReward = noteEvent.getReward()
|
||||||
|
if (baseReward != null) {
|
||||||
|
DisplayReward(baseReward, baseNote, account, navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(3.dp))
|
Spacer(modifier = Modifier.height(3.dp))
|
||||||
@@ -394,7 +400,7 @@ fun NoteCompose(
|
|||||||
thickness = 0.25.dp
|
thickness = 0.25.dp
|
||||||
)
|
)
|
||||||
} else if (noteEvent is LongTextNoteEvent) {
|
} else if (noteEvent is LongTextNoteEvent) {
|
||||||
LongFormHeader(noteEvent)
|
LongFormHeader(noteEvent, note, account.userProfile())
|
||||||
|
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel)
|
||||||
|
|
||||||
@@ -473,15 +479,26 @@ fun NoteCompose(
|
|||||||
!noteForReports.hasAnyReports()
|
!noteForReports.hasAnyReports()
|
||||||
|
|
||||||
if (eventContent != null) {
|
if (eventContent != null) {
|
||||||
TranslateableRichTextViewer(
|
if (makeItShort && note.author == account.userProfile()) {
|
||||||
eventContent,
|
Text(
|
||||||
canPreview = canPreview && !makeItShort,
|
text = eventContent,
|
||||||
Modifier.fillMaxWidth(),
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
noteEvent.tags(),
|
maxLines = 2,
|
||||||
backgroundColor,
|
overflow = TextOverflow.Ellipsis
|
||||||
accountViewModel,
|
)
|
||||||
navController
|
} else {
|
||||||
)
|
TranslateableRichTextViewer(
|
||||||
|
eventContent,
|
||||||
|
canPreview = canPreview && !makeItShort,
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
noteEvent.tags(),
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
|
||||||
|
DisplayUncitedHashtags(noteEvent, eventContent, navController)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!makeItShort) {
|
if (!makeItShort) {
|
||||||
@@ -501,6 +518,130 @@ fun NoteCompose(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayFollowingHashtagsInPost(
|
||||||
|
noteEvent: EventInterface,
|
||||||
|
account: Account,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
Column() {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
val firstTag =
|
||||||
|
noteEvent.firstIsTaggedHashes(account.followingTagSet())
|
||||||
|
if (firstTag != null) {
|
||||||
|
ClickableText(
|
||||||
|
text = AnnotatedString(" #$firstTag"),
|
||||||
|
onClick = { navController.navigate("Hashtag/$firstTag") },
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
color = MaterialTheme.colors.primary.copy(
|
||||||
|
alpha = 0.52f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayUncitedHashtags(
|
||||||
|
noteEvent: EventInterface,
|
||||||
|
eventContent: String,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
val hashtags = noteEvent.hashtags()
|
||||||
|
if (hashtags.isNotEmpty()) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(top = 5.dp)
|
||||||
|
) {
|
||||||
|
hashtags.forEach { hashtag ->
|
||||||
|
if (!eventContent.contains(hashtag, true)) {
|
||||||
|
ClickableText(
|
||||||
|
text = AnnotatedString("#$hashtag "),
|
||||||
|
onClick = { navController.navigate("Hashtag/$hashtag") },
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
color = MaterialTheme.colors.primary.copy(
|
||||||
|
alpha = 0.52f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayReward(
|
||||||
|
baseReward: BigDecimal,
|
||||||
|
baseNote: Note,
|
||||||
|
account: Account,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.clickable { popupExpanded = true }
|
||||||
|
) {
|
||||||
|
ClickableText(
|
||||||
|
text = AnnotatedString("#bounty"),
|
||||||
|
onClick = { navController.navigate("Hashtag/bounty") },
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
color = MaterialTheme.colors.primary.copy(
|
||||||
|
alpha = 0.52f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val repliesState by baseNote.live().replies.observeAsState()
|
||||||
|
val replyNote = repliesState?.note
|
||||||
|
|
||||||
|
if (replyNote?.hasPledgeBy(account.userProfile()) == true) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Bolt,
|
||||||
|
contentDescription = stringResource(R.string.zaps),
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = BitcoinOrange
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Bolt,
|
||||||
|
contentDescription = stringResource(R.string.zaps),
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rewardAmount by remember {
|
||||||
|
mutableStateOf<BigDecimal?>(
|
||||||
|
baseReward
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = repliesState) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
replyNote?.pledgedAmountByOthers()?.let {
|
||||||
|
rewardAmount = baseReward.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
showAmount(rewardAmount),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (popupExpanded) {
|
||||||
|
AddBountyAmountDialog(baseNote, account) {
|
||||||
|
popupExpanded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BadgeDisplay(baseNote: Note) {
|
fun BadgeDisplay(baseNote: Note) {
|
||||||
val background = MaterialTheme.colors.background
|
val background = MaterialTheme.colors.background
|
||||||
@@ -565,7 +706,7 @@ fun BadgeDisplay(baseNote: Note) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LongFormHeader(noteEvent: LongTextNoteEvent) {
|
private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, loggedIn: User) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(shape = RoundedCornerShape(15.dp))
|
.clip(shape = RoundedCornerShape(15.dp))
|
||||||
@@ -586,6 +727,35 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent) {
|
|||||||
contentScale = ContentScale.FillWidth,
|
contentScale = ContentScale.FillWidth,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
} ?: Box() {
|
||||||
|
note.author?.info?.banner?.let {
|
||||||
|
AsyncImage(
|
||||||
|
model = it,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
R.string.preview_card_image_for,
|
||||||
|
it
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
} ?: Image(
|
||||||
|
painter = painterResource(R.drawable.profile_banner),
|
||||||
|
contentDescription = stringResource(R.string.profile_banner),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(150.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.width(75.dp)
|
||||||
|
.height(75.dp)
|
||||||
|
.padding(10.dp)
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
) {
|
||||||
|
NoteAuthorPicture(baseNote = note, baseUserAccount = loggedIn, size = 55.dp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
noteEvent.title()?.let {
|
noteEvent.title()?.let {
|
||||||
@@ -610,6 +780,16 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent) {
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
?: Text(
|
||||||
|
text = noteEvent.content.take(200),
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ private fun lightenColor(color: Color, amount: Float): Color {
|
|||||||
|
|
||||||
val externalLinkForNote = { note: Note ->
|
val externalLinkForNote = { note: Note ->
|
||||||
if (note is AddressableNote) {
|
if (note is AddressableNote) {
|
||||||
"https://habla.news/a/${note.address().toNAddr()}"
|
if (note.event?.getReward() != null) {
|
||||||
|
"https://nostrbounties.com/b/${note.address().toNAddr()}"
|
||||||
|
} else {
|
||||||
|
"https://habla.news/a/${note.address().toNAddr()}"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
"https://snort.social/e/${note.idNote()}"
|
"https://snort.social/e/${note.idNote()}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.qrcode
|
package com.vitorpamplona.amethyst.ui.qrcode
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -19,6 +20,8 @@ fun NIP19QrCodeScanner(onScan: (String?) -> Unit) {
|
|||||||
val startingPage = when (nip19?.type) {
|
val startingPage = when (nip19?.type) {
|
||||||
Nip19.Type.USER -> "User/${nip19.hex}"
|
Nip19.Type.USER -> "User/${nip19.hex}"
|
||||||
Nip19.Type.NOTE -> "Note/${nip19.hex}"
|
Nip19.Type.NOTE -> "Note/${nip19.hex}"
|
||||||
|
Nip19.Type.EVENT -> "Event/${nip19.hex}"
|
||||||
|
Nip19.Type.ADDRESS -> "Note/${nip19.hex}"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +31,7 @@ fun NIP19QrCodeScanner(onScan: (String?) -> Unit) {
|
|||||||
onScan(null)
|
onScan(null)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
Log.e("NIP19 Scanner", "Error parsing $it", e)
|
||||||
// QR can be anything, do not throw errors.
|
// QR can be anything, do not throw errors.
|
||||||
onScan(null)
|
onScan(null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
|||||||
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
|
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.DisplayReward
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags
|
||||||
import com.vitorpamplona.amethyst.ui.note.HiddenNote
|
import com.vitorpamplona.amethyst.ui.note.HiddenNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
@@ -250,6 +253,8 @@ fun NoteMaster(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
||||||
|
|
||||||
|
DisplayFollowingHashtagsInPost(noteEvent, account, navController)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
timeAgo(note.createdAt(), context = context),
|
timeAgo(note.createdAt(), context = context),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
@@ -271,7 +276,14 @@ fun NoteMaster(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ObserveDisplayNip05Status(baseNote)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
ObserveDisplayNip05Status(baseNote, Modifier.weight(1f))
|
||||||
|
|
||||||
|
val baseReward = noteEvent.getReward()
|
||||||
|
if (baseReward != null) {
|
||||||
|
DisplayReward(baseReward, baseNote, account, navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +353,8 @@ fun NoteMaster(
|
|||||||
accountViewModel,
|
accountViewModel,
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DisplayUncitedHashtags(noteEvent, eventContent, navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel)
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadRedirectScreen(eventId: String?, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
|
if (eventId == null) return
|
||||||
|
|
||||||
|
val baseNote = LocalCache.checkGetOrCreateNote(eventId) ?: return
|
||||||
|
|
||||||
|
val noteState by baseNote.live().metadata.observeAsState()
|
||||||
|
val note = noteState?.note
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = noteState) {
|
||||||
|
val event = note?.event
|
||||||
|
val channel = note?.channel()
|
||||||
|
|
||||||
|
if (event == null) {
|
||||||
|
// stay here, loading
|
||||||
|
} else if (event is ChannelCreateEvent) {
|
||||||
|
navController.backQueue.removeLast()
|
||||||
|
navController.navigate("Channel/${note.idHex}")
|
||||||
|
} else if (event is PrivateDmEvent) {
|
||||||
|
navController.backQueue.removeLast()
|
||||||
|
navController.navigate("Room/${note.author?.pubkeyHex}")
|
||||||
|
} else if (channel != null) {
|
||||||
|
navController.backQueue.removeLast()
|
||||||
|
navController.navigate("Channel/${channel.idHex}")
|
||||||
|
} else {
|
||||||
|
navController.backQueue.removeLast()
|
||||||
|
navController.navigate("Note/${note.idHex}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 50.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.looking_for_event, eventId))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
|
|||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
|
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
|
||||||
@@ -393,7 +394,7 @@ private fun ProfileHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawAdditionalInfo(baseUser, account, navController)
|
DrawAdditionalInfo(baseUser, account, accountViewModel, navController)
|
||||||
|
|
||||||
Divider(modifier = Modifier.padding(top = 6.dp))
|
Divider(modifier = Modifier.padding(top = 6.dp))
|
||||||
}
|
}
|
||||||
@@ -406,7 +407,7 @@ private fun ProfileHeader(
|
|||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun DrawAdditionalInfo(baseUser: User, account: Account, navController: NavController) {
|
private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
val userState by baseUser.live().metadata.observeAsState()
|
val userState by baseUser.live().metadata.observeAsState()
|
||||||
val user = userState?.user ?: return
|
val user = userState?.user ?: return
|
||||||
|
|
||||||
@@ -554,11 +555,18 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, navController:
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.info?.about?.let {
|
user.info?.about?.let {
|
||||||
Text(
|
Row(
|
||||||
it,
|
|
||||||
color = MaterialTheme.colors.onSurface,
|
|
||||||
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
|
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
|
||||||
)
|
) {
|
||||||
|
TranslateableRichTextViewer(
|
||||||
|
content = it,
|
||||||
|
canPreview = false,
|
||||||
|
tags = null,
|
||||||
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<string name="copy_user_pubkey">Copiar PubKey do usuário</string>
|
<string name="copy_user_pubkey">Copiar PubKey do usuário</string>
|
||||||
<string name="copy_note_id">Copiar ID da Nota</string>
|
<string name="copy_note_id">Copiar ID da Nota</string>
|
||||||
<string name="broadcast">Transmitir</string>
|
<string name="broadcast">Transmitir</string>
|
||||||
|
<string name="request_deletion">Pedir para excluir</string>
|
||||||
<string name="block_hide_user">Bloquear e ocultar usuário</string>
|
<string name="block_hide_user">Bloquear e ocultar usuário</string>
|
||||||
<string name="report_spam_scam">Denunciar spam/fraude</string>
|
<string name="report_spam_scam">Denunciar spam/fraude</string>
|
||||||
<string name="report_impersonation">Denunciar representação</string>
|
<string name="report_impersonation">Denunciar representação</string>
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
<string name="failed_to_upload_the_image">Falha ao enviar imagem</string>
|
<string name="failed_to_upload_the_image">Falha ao enviar imagem</string>
|
||||||
<string name="relay_address">Endereço do Relay</string>
|
<string name="relay_address">Endereço do Relay</string>
|
||||||
<string name="posts">Posts</string>
|
<string name="posts">Posts</string>
|
||||||
|
<string name="bytes">Bytes</string>
|
||||||
<string name="errors">Erros</string>
|
<string name="errors">Erros</string>
|
||||||
<string name="home_feed">Feed principal</string>
|
<string name="home_feed">Feed principal</string>
|
||||||
<string name="private_message_feed">Feed de mensagens privadas</string>
|
<string name="private_message_feed">Feed de mensagens privadas</string>
|
||||||
@@ -191,6 +193,14 @@
|
|||||||
<string name="copied_user_id_to_clipboard">Copiado @npub do autor</string>
|
<string name="copied_user_id_to_clipboard">Copiado @npub do autor</string>
|
||||||
<string name="copied_note_id_to_clipboard">Copiado ID da nota (@note1)</string>
|
<string name="copied_note_id_to_clipboard">Copiado ID da nota (@note1)</string>
|
||||||
<string name="select_text_dialog_top">Selecionar texto</string>
|
<string name="select_text_dialog_top">Selecionar texto</string>
|
||||||
|
<string name="account_switch_add_account_dialog_title">Adicionar Nova Conta</string>
|
||||||
|
<string name="drawer_accounts">Contas</string>
|
||||||
|
<string name="account_switch_select_account">Selecionar Conta</string>
|
||||||
|
<string name="account_switch_add_account_btn">Adicionar Nova Conta</string>
|
||||||
|
<string name="account_switch_active_account">Ativar conta</string>
|
||||||
|
<string name="account_switch_has_private_key">Tem chave privada</string>
|
||||||
|
<string name="account_switch_pubkey_only">Somente leitura, sem chave privada</string>
|
||||||
|
<string name="back">Voltar</string>
|
||||||
<string name="quick_action_select">Selecionar</string>
|
<string name="quick_action_select">Selecionar</string>
|
||||||
<string name="quick_action_share_browser_link">Compartilhar link do navegador</string>
|
<string name="quick_action_share_browser_link">Compartilhar link do navegador</string>
|
||||||
<string name="quick_action_share">Compartilhar</string>
|
<string name="quick_action_share">Compartilhar</string>
|
||||||
@@ -204,6 +214,38 @@
|
|||||||
<string name="quick_action_request_deletion_alert_body">Amethyst solicitará que sua nota seja excluída dos relays aos quais você está conectado no momento. Não há garantia de que sua nota será excluída permanentemente desses relays ou de outros relays onde possa estar armazenada.</string>
|
<string name="quick_action_request_deletion_alert_body">Amethyst solicitará que sua nota seja excluída dos relays aos quais você está conectado no momento. Não há garantia de que sua nota será excluída permanentemente desses relays ou de outros relays onde possa estar armazenada.</string>
|
||||||
<string name="backup_keys">Copia de segurança das chaves</string>
|
<string name="backup_keys">Copia de segurança das chaves</string>
|
||||||
<string name="private_conversation_notification"><Não foi possível descriptografar a mensagem privada>\\n\\nVocê foi citado em uma conversa privada/criptografada entre %1$s and %2$s.</string>
|
<string name="private_conversation_notification"><Não foi possível descriptografar a mensagem privada>\\n\\nVocê foi citado em uma conversa privada/criptografada entre %1$s and %2$s.</string>
|
||||||
|
<string name="quick_action_block_dialog_btn">Bloquear</string>
|
||||||
|
<string name="quick_action_delete_dialog_btn">Excluir</string>
|
||||||
|
<string name="quick_action_block">Bloquear</string>
|
||||||
|
<string name="quick_action_report">Denunciar</string>
|
||||||
<string name="quick_action_delete_button">Excluir</string>
|
<string name="quick_action_delete_button">Excluir</string>
|
||||||
<string name="quick_action_dont_show_again_button">Não mostar novamente</string>
|
<string name="quick_action_dont_show_again_button">Não mostar novamente</string>
|
||||||
|
<string name="report_dialog_spam">Spam ou golpes</string>
|
||||||
|
<string name="report_dialog_profanity">Palavrões ou conduta odiosa</string>
|
||||||
|
<string name="report_dialog_impersonation">Personificação maliciosa</string>
|
||||||
|
<string name="report_dialog_nudity">Nudez ou conteúdo gráfico</string>
|
||||||
|
<string name="report_dialog_illegal">Comportamento ilegal</string>
|
||||||
|
<string name="report_dialog_blocking_a_user">Bloquear um usuário ocultará seu conteúdo em seu aplicativo. Suas anotações ainda podem ser visualizadas publicamente, inclusive para as pessoas que você bloquear. Os usuários bloqueados são listados na tela Filtros de segurança.</string>
|
||||||
|
<string name="report_dialog_block_hide_user_btn"><![CDATA[Bloquear e ocultar usuário]]></string>
|
||||||
|
<string name="report_dialog_report_btn">Denunciar abuso</string>
|
||||||
|
<string name="report_dialog_reminder_public">Todos as denuncias postadas serão visíveis publicamente.</string>
|
||||||
|
<string name="report_dialog_additional_reason_placeholder">Opcionalmente, forneça contexto adicional sobre a sua denuncia…</string>
|
||||||
|
<string name="report_dialog_additional_reason_label">Contexto Adicional</string>
|
||||||
|
<string name="report_dialog_select_reason_label">Motivo</string>
|
||||||
|
<string name="report_dialog_select_reason_placeholder">Selecione um motivo…</string>
|
||||||
|
<string name="report_dialog_post_report_btn">Enviar Denuncia</string>
|
||||||
|
<string name="report_dialog_title">Bloquear e Denunciar</string>
|
||||||
|
<string name="block_only">Bloquear</string>
|
||||||
|
<string name="bookmarks">Itens Salvos</string>
|
||||||
|
<string name="private_bookmarks">Itens Salvos Privados</string>
|
||||||
|
<string name="public_bookmarks">Itens Salvos Públicos</string>
|
||||||
|
<string name="add_to_private_bookmarks">Adicionar aos Itens Salvos Privados</string>
|
||||||
|
<string name="add_to_public_bookmarks">Adicionar aos Itens Salvos Públicos</string>
|
||||||
|
<string name="remove_from_private_bookmarks">Remover dos Itens Salvos Privados</string>
|
||||||
|
<string name="remove_from_public_bookmarks">Remover dos Itens Salvos Públicos</string>
|
||||||
|
<string name="wallet_connect_service">Serviço Wallet Connect</string>
|
||||||
|
<string name="wallet_connect_service_explainer">Use sua chave privada para pagar zaps sem sair do app. Qualquer pessoa com acesso à sua chave privada Nostr poderá gastar o saldo da sua carteira. Guarde apenas os fundos que você pode perder e use um retransmissor privado, se possível. O operador de Relay pode ver seus metadados de pagamentos.</string>
|
||||||
|
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
|
||||||
|
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
|
||||||
|
<string name="pledge_amount_in_sats">Valor em Sats</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
<string name="failed_to_upload_the_image">Failed to upload the image</string>
|
<string name="failed_to_upload_the_image">Failed to upload the image</string>
|
||||||
<string name="relay_address">Relay Address</string>
|
<string name="relay_address">Relay Address</string>
|
||||||
<string name="posts">Posts</string>
|
<string name="posts">Posts</string>
|
||||||
|
<string name="bytes">Bytes</string>
|
||||||
<string name="errors">Errors</string>
|
<string name="errors">Errors</string>
|
||||||
<string name="home_feed">Home Feed</string>
|
<string name="home_feed">Home Feed</string>
|
||||||
<string name="private_message_feed">Private Message Feed</string>
|
<string name="private_message_feed">Private Message Feed</string>
|
||||||
@@ -260,4 +261,8 @@
|
|||||||
<string name="wallet_connect_service_explainer">Uses your private key to pay zaps without leaving the app. Anyone with access to your Nostr private key will be able to spend your wallet\'s balance. Only keep funds you are ok to lose and use a private relay if possible. The relay operator can see your payments metadata.</string>
|
<string name="wallet_connect_service_explainer">Uses your private key to pay zaps without leaving the app. Anyone with access to your Nostr private key will be able to spend your wallet\'s balance. Only keep funds you are ok to lose and use a private relay if possible. The relay operator can see your payments metadata.</string>
|
||||||
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
|
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
|
||||||
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
|
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
|
||||||
|
|
||||||
|
<string name="pledge_amount_in_sats">Pledge Amount in Sats</string>
|
||||||
|
|
||||||
|
<string name="looking_for_event">"Looking for Event %1$s"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+1
-1
@@ -102,7 +102,7 @@ object LanguageTranslatorService {
|
|||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
try {
|
try {
|
||||||
val tag = matcher.group()
|
val tag = matcher.group()
|
||||||
val short = "Amethysttagindexer$counter"
|
val short = "Amethystmindexer$counter"
|
||||||
returningList.put(short, tag)
|
returningList.put(short, tag)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,4 +69,12 @@ class NIP19ParserTest {
|
|||||||
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418", result?.hex)
|
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418", result?.hex)
|
||||||
assertEquals(null, result?.relay)
|
assertEquals(null, result?.relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nEventParserTest() {
|
||||||
|
val result = Nip19.uriToRoute("nostr:nevent1qqs0tsw8hjacs4fppgdg7f5yhgwwfkyua4xcs3re9wwkpkk2qeu6mhql22rcy")
|
||||||
|
assertEquals(Nip19.Type.EVENT, result?.type)
|
||||||
|
assertEquals("f5c1c7bcbb8855210a1a8f2684ba1ce4d89ced4d8844792b9d60daca0679addc", result?.hex)
|
||||||
|
assertEquals(null, result?.relay)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user