Merge branch 'main' into main
This commit is contained in:
+2
-2
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId "com.vitorpamplona.amethyst"
|
applicationId "com.vitorpamplona.amethyst"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 113
|
versionCode 114
|
||||||
versionName "0.31.3"
|
versionName "0.31.4"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.util.Log
|
|||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.model.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ class AntiSpamFilter {
|
|||||||
val spamMessages = LruCache<Int, Spammer>(1000)
|
val spamMessages = LruCache<Int, Spammer>(1000)
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun isSpam(event: Event): Boolean {
|
fun isSpam(event: Event, relay: Relay?): Boolean {
|
||||||
val idHex = event.id
|
val idHex = event.id
|
||||||
|
|
||||||
// if short message, ok
|
// if short message, ok
|
||||||
@@ -27,7 +28,7 @@ class AntiSpamFilter {
|
|||||||
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
|
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
|
||||||
|
|
||||||
if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) {
|
if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) {
|
||||||
Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}")
|
Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}")
|
||||||
|
|
||||||
// Log down offenders
|
// Log down offenders
|
||||||
if (spamMessages.get(hash) == null) {
|
if (spamMessages.get(hash) == null) {
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ fun HexKey.toDisplayHexKey(): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun decodePublicKey(key: String): ByteArray {
|
fun decodePublicKey(key: String): ByteArray {
|
||||||
|
val parsed = Nip19.uriToRoute(key)
|
||||||
|
val pubKeyParsed = parsed?.hex?.toByteArray()
|
||||||
|
|
||||||
return if (key.startsWith("nsec")) {
|
return if (key.startsWith("nsec")) {
|
||||||
Persona(privKey = key.bechToBytes()).pubKey
|
Persona(privKey = key.bechToBytes()).pubKey
|
||||||
} else if (key.startsWith("npub")) {
|
} else if (pubKeyParsed != null) {
|
||||||
key.bechToBytes()
|
pubKeyParsed
|
||||||
} else if (key.startsWith("note")) {
|
} else {
|
||||||
key.bechToBytes()
|
|
||||||
} else { // if (pattern.matcher(key).matches()) {
|
|
||||||
// } else {
|
|
||||||
Hex.decode(key)
|
Hex.decode(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ object LocalCache {
|
|||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
if (antiSpam.isSpam(event)) {
|
if (antiSpam.isSpam(event, relay)) {
|
||||||
relay?.let {
|
relay?.let {
|
||||||
it.spamCounter++
|
it.spamCounter++
|
||||||
}
|
}
|
||||||
@@ -202,7 +202,7 @@ object LocalCache {
|
|||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event?.id() == event.id()) return
|
if (note.event?.id() == event.id()) return
|
||||||
|
|
||||||
if (antiSpam.isSpam(event)) {
|
if (antiSpam.isSpam(event, relay)) {
|
||||||
relay?.let {
|
relay?.let {
|
||||||
it.spamCounter++
|
it.spamCounter++
|
||||||
}
|
}
|
||||||
@@ -317,12 +317,10 @@ object LocalCache {
|
|||||||
|
|
||||||
fun consume(event: ContactListEvent) {
|
fun consume(event: ContactListEvent) {
|
||||||
val user = getOrCreateUser(event.pubKey)
|
val user = getOrCreateUser(event.pubKey)
|
||||||
val follows = event.unverifiedFollowKeySet()
|
|
||||||
|
|
||||||
if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !follows.isNullOrEmpty()) {
|
// avoids processing empty contact lists.
|
||||||
// Saves relay list only if it's a user that is currently been seen
|
if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !event.tags.isEmpty()) {
|
||||||
user.updateContactList(event)
|
user.updateContactList(event)
|
||||||
|
|
||||||
// Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
|
// Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,7 +556,7 @@ object LocalCache {
|
|||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
if (antiSpam.isSpam(event)) {
|
if (antiSpam.isSpam(event, relay)) {
|
||||||
relay?.let {
|
relay?.let {
|
||||||
it.spamCounter++
|
it.spamCounter++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ open class Note(val idHex: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun addZap(zapRequest: Note, zap: Note?) {
|
fun addZap(zapRequest: Note, zap: Note?) {
|
||||||
if (zapRequest !in zaps.keys) {
|
if (zapRequest !in zaps.keys) {
|
||||||
zaps = zaps + Pair(zapRequest, zap)
|
zaps = zaps + Pair(zapRequest, zap)
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ class User(val pubkeyHex: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun transientFollowerCount(): Int {
|
fun transientFollowerCount(): Int {
|
||||||
return LocalCache.users.values.count { it.latestContactList?.let { pubkeyHex in it.unverifiedFollowKeySet() } ?: false }
|
return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cachedFollowingKeySet(): Set<HexKey> {
|
fun cachedFollowingKeySet(): Set<HexKey> {
|
||||||
@@ -289,7 +289,7 @@ class User(val pubkeyHex: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cachedFollowerCount(): Int {
|
fun cachedFollowerCount(): Int {
|
||||||
return LocalCache.users.values.count { it.latestContactList?.let { pubkeyHex in it.unverifiedFollowKeySet() } ?: false }
|
return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasSentMessagesTo(user: User?): Boolean {
|
fun hasSentMessagesTo(user: User?): Boolean {
|
||||||
|
|||||||
@@ -40,9 +40,13 @@ class LnZapEvent(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun message(): String {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
val message = content
|
||||||
|
|
||||||
override fun containedPost(): Event? = try {
|
override fun containedPost(): Event? = try {
|
||||||
description()?.let {
|
description()?.ifBlank { null }?.let {
|
||||||
fromJson(it, Client.lenient)
|
fromJson(it, Client.lenient)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -63,4 +67,4 @@ class LnZapEvent(
|
|||||||
companion object {
|
companion object {
|
||||||
const val kind = 9735
|
const val kind = 9735
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,4 +15,6 @@ interface LnZapEventInterface : EventInterface {
|
|||||||
fun amount(): BigDecimal?
|
fun amount(): BigDecimal?
|
||||||
|
|
||||||
fun containedPost(): Event?
|
fun containedPost(): Event?
|
||||||
|
|
||||||
|
fun message(): String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package com.vitorpamplona.amethyst.service.relays
|
|||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
|
||||||
class EOSETime(var time: Long)
|
class EOSETime(var time: Long) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return time.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class EOSERelayList(var relayList: Map<String, EOSETime> = emptyMap()) {
|
class EOSERelayList(var relayList: Map<String, EOSETime> = emptyMap()) {
|
||||||
fun addOrUpdate(relayUrl: String, time: Long) {
|
fun addOrUpdate(relayUrl: String, time: Long) {
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||||||
if (isValidURL(myUrlPreview)) {
|
if (isValidURL(myUrlPreview)) {
|
||||||
val removedParamsFromUrl =
|
val removedParamsFromUrl =
|
||||||
myUrlPreview.split("?")[0].lowercase()
|
myUrlPreview.split("?")[0].lowercase()
|
||||||
if (imageExtensions.any { removedParamsFromUrl.endsWith(it, true) }) {
|
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = myUrlPreview,
|
model = myUrlPreview,
|
||||||
contentDescription = myUrlPreview,
|
contentDescription = myUrlPreview,
|
||||||
@@ -182,7 +182,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||||||
RoundedCornerShape(15.dp)
|
RoundedCornerShape(15.dp)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it, true) }) {
|
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
VideoView(myUrlPreview)
|
VideoView(myUrlPreview)
|
||||||
} else {
|
} else {
|
||||||
UrlPreview(myUrlPreview, myUrlPreview)
|
UrlPreview(myUrlPreview, myUrlPreview)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import androidx.compose.material.LocalTextStyle
|
|||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -138,10 +137,10 @@ fun RichTextViewer(
|
|||||||
// sequence of images will render in a slideview
|
// sequence of images will render in a slideview
|
||||||
if (isValidURL(word)) {
|
if (isValidURL(word)) {
|
||||||
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
||||||
if (imageExtensions.any { word.endsWith(it, true) }) {
|
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
imagesForPager.add(word)
|
imagesForPager.add(word)
|
||||||
}
|
}
|
||||||
if (videoExtensions.any { word.endsWith(it, true) }) {
|
if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
imagesForPager.add(word)
|
imagesForPager.add(word)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,28 +154,59 @@ fun RichTextViewer(
|
|||||||
s.forEach { word: String ->
|
s.forEach { word: String ->
|
||||||
if (canPreview) {
|
if (canPreview) {
|
||||||
// Explicit URL
|
// Explicit URL
|
||||||
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
|
||||||
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
|
|
||||||
|
|
||||||
if (isValidURL(word)) {
|
if (isValidURL(word)) {
|
||||||
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
||||||
if (imageExtensions.any { word.endsWith(it, true) }) {
|
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
ZoomableImageView(word, imagesForPager)
|
ZoomableImageView(word, imagesForPager)
|
||||||
} else if (videoExtensions.any { word.endsWith(it, true) }) {
|
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
ZoomableImageView(word, imagesForPager)
|
ZoomableImageView(word, imagesForPager)
|
||||||
} else {
|
} else {
|
||||||
UrlPreview(word, "$word ")
|
UrlPreview(word, "$word ")
|
||||||
}
|
}
|
||||||
} else if (lnInvoice != null) {
|
} else if (word.startsWith("lnbc", true)) {
|
||||||
InvoicePreview(lnInvoice)
|
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
||||||
} else if (lnWithdrawal != null) {
|
if (lnInvoice != null) {
|
||||||
ClickableWithdrawal(withdrawalString = lnWithdrawal)
|
InvoicePreview(lnInvoice)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (word.startsWith("lnurl", true)) {
|
||||||
|
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
|
||||||
|
if (lnWithdrawal != null) {
|
||||||
|
ClickableWithdrawal(withdrawalString = lnWithdrawal)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
} 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 (word.length > 6 && Patterns.PHONE.matcher(word).matches()) {
|
||||||
ClickablePhone(word)
|
ClickablePhone(word)
|
||||||
} else if (isBechLink(word)) {
|
} else if (isBechLink(word)) {
|
||||||
BechLink(word, navController)
|
BechLink(word, navController)
|
||||||
|
} else if (word.startsWith("#")) {
|
||||||
|
if (tagIndex.matcher(word).matches() && tags != null) {
|
||||||
|
TagLink(
|
||||||
|
word,
|
||||||
|
tags,
|
||||||
|
canPreview,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
} else if (hashTagsPattern.matcher(word).matches()) {
|
||||||
|
HashTag(word, accountViewModel, navController)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||||
val matcher = noProtocolUrlValidator.matcher(word)
|
val matcher = noProtocolUrlValidator.matcher(word)
|
||||||
matcher.find()
|
matcher.find()
|
||||||
@@ -185,10 +215,6 @@ fun RichTextViewer(
|
|||||||
|
|
||||||
ClickableUrl(url, "https://$url")
|
ClickableUrl(url, "https://$url")
|
||||||
Text("$additionalChars ")
|
Text("$additionalChars ")
|
||||||
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
|
||||||
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
|
|
||||||
} else if (hashTagsPattern.matcher(word).matches()) {
|
|
||||||
HashTag(word, accountViewModel, navController)
|
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = "$word ",
|
text = "$word ",
|
||||||
@@ -198,12 +224,40 @@ fun RichTextViewer(
|
|||||||
} else {
|
} else {
|
||||||
if (isValidURL(word)) {
|
if (isValidURL(word)) {
|
||||||
ClickableUrl("$word ", word)
|
ClickableUrl("$word ", word)
|
||||||
|
} else if (word.startsWith("lnurl", true)) {
|
||||||
|
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
|
||||||
|
if (lnWithdrawal != null) {
|
||||||
|
ClickableWithdrawal(withdrawalString = lnWithdrawal)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
} 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) {
|
||||||
ClickablePhone(word)
|
ClickablePhone(word)
|
||||||
} else if (isBechLink(word)) {
|
} else if (isBechLink(word)) {
|
||||||
BechLink(word, navController)
|
BechLink(word, navController)
|
||||||
|
} else if (word.startsWith("#")) {
|
||||||
|
if (tagIndex.matcher(word).matches() && tags != null) {
|
||||||
|
TagLink(
|
||||||
|
word,
|
||||||
|
tags,
|
||||||
|
canPreview,
|
||||||
|
backgroundColor,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
} else if (hashTagsPattern.matcher(word).matches()) {
|
||||||
|
HashTag(word, accountViewModel, navController)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||||
val matcher = noProtocolUrlValidator.matcher(word)
|
val matcher = noProtocolUrlValidator.matcher(word)
|
||||||
matcher.find()
|
matcher.find()
|
||||||
@@ -212,10 +266,6 @@ fun RichTextViewer(
|
|||||||
|
|
||||||
ClickableUrl(url, "https://$url")
|
ClickableUrl(url, "https://$url")
|
||||||
Text("$additionalChars ")
|
Text("$additionalChars ")
|
||||||
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
|
||||||
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
|
|
||||||
} else if (hashTagsPattern.matcher(word).matches()) {
|
|
||||||
HashTag(word, accountViewModel, navController)
|
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = "$word ",
|
text = "$word ",
|
||||||
@@ -235,9 +285,9 @@ private fun isArabic(text: String): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isBechLink(word: String): Boolean {
|
fun isBechLink(word: String): Boolean {
|
||||||
val cleaned = word.removePrefix("@").removePrefix("nostr:").removePrefix("@")
|
val cleaned = word.removePrefix("@").removePrefix("nostr:").removePrefix("@").take(7).lowercase()
|
||||||
|
|
||||||
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it, true) }
|
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -340,14 +390,8 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
|
|||||||
if (tags[index][0] == "p") {
|
if (tags[index][0] == "p") {
|
||||||
val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1])
|
val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1])
|
||||||
if (baseUser != null) {
|
if (baseUser != null) {
|
||||||
val userState = baseUser.live().metadata.observeAsState()
|
ClickableUserTag(baseUser, navController)
|
||||||
val user = userState.value?.user
|
Text(text = "$extraCharacters ")
|
||||||
if (user != null) {
|
|
||||||
ClickableUserTag(user, navController)
|
|
||||||
Text(text = "$extraCharacters ")
|
|
||||||
} else {
|
|
||||||
Text(text = "$word ")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// if here the tag is not a valid Nostr Hex
|
// if here the tag is not a valid Nostr Hex
|
||||||
Text(text = "$word ")
|
Text(text = "$word ")
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ fun ZoomableImageView(word: String, images: List<String> = listOf(word)) {
|
|||||||
mutableStateOf<AsyncImagePainter.State?>(null)
|
mutableStateOf<AsyncImagePainter.State?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageExtensions.any { word.endsWith(it, true) }) {
|
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
||||||
|
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = word,
|
model = word,
|
||||||
contentDescription = word,
|
contentDescription = word,
|
||||||
@@ -171,7 +172,8 @@ fun ZoomableImageDialog(imageUrl: String, allImages: List<String> = listOf(image
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderImageOrVideo(imageUrl: String) {
|
private fun RenderImageOrVideo(imageUrl: String) {
|
||||||
if (imageExtensions.any { imageUrl.endsWith(it, true) }) {
|
val removedParamsFromUrl = imageUrl.split("?")[0].lowercase()
|
||||||
|
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = imageUrl,
|
model = imageUrl,
|
||||||
contentDescription = stringResource(id = R.string.profile_image),
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ fun NoteCompose(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("Time", "Note Compose in $elapsed for ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}")
|
Log.d("Time", "Note Compose in $elapsed for ${baseNote.event?.kind()} ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 10.dp),
|
.padding(horizontal = 10.dp),
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceAround
|
||||||
) {
|
) {
|
||||||
if (presenting) {
|
if (presenting) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
@@ -91,22 +91,22 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
|||||||
fontSize = 18.sp
|
fontSize = 18.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 35.dp, vertical = 10.dp)
|
|
||||||
) {
|
|
||||||
QrCodeDrawer("nostr:${user.pubkeyNpub()}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 30.dp, vertical = 10.dp)
|
.padding(horizontal = 35.dp)
|
||||||
|
) {
|
||||||
|
QrCodeDrawer("nostr:${user.pubkeyNpub()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 30.dp)
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { presenting = false },
|
onClick = { presenting = false },
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel
|
|||||||
import com.vitorpamplona.amethyst.LocalPreferences
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.ServiceManager
|
import com.vitorpamplona.amethyst.ServiceManager
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
@@ -39,12 +41,14 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
|
|
||||||
fun login(key: String) {
|
fun login(key: String) {
|
||||||
val pattern = Pattern.compile(".+@.+\\.[a-z]+")
|
val pattern = Pattern.compile(".+@.+\\.[a-z]+")
|
||||||
|
val parsed = Nip19.uriToRoute(key)
|
||||||
|
val pubKeyParsed = parsed?.hex?.toByteArray()
|
||||||
|
|
||||||
val account =
|
val account =
|
||||||
if (key.startsWith("nsec")) {
|
if (key.startsWith("nsec")) {
|
||||||
Account(Persona(privKey = key.bechToBytes()))
|
Account(Persona(privKey = key.bechToBytes()))
|
||||||
} else if (key.startsWith("npub")) {
|
} else if (pubKeyParsed != null) {
|
||||||
Account(Persona(pubKey = key.bechToBytes()))
|
Account(Persona(pubKey = pubKeyParsed))
|
||||||
} else if (pattern.matcher(key).matches()) {
|
} else if (pattern.matcher(key).matches()) {
|
||||||
// Evaluate NIP-5
|
// Evaluate NIP-5
|
||||||
Account(Persona())
|
Account(Persona())
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
||||||
@@ -450,16 +451,41 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
|
|||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(30.dp)
|
.size(25.dp)
|
||||||
.padding(start = 5.dp),
|
.padding(start = 5.dp),
|
||||||
onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); }
|
onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); }
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ContentCopy,
|
imageVector = Icons.Default.ContentCopy,
|
||||||
null,
|
null,
|
||||||
modifier = Modifier
|
modifier = Modifier.size(15.dp),
|
||||||
.padding(end = 5.dp)
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
.size(15.dp),
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialogOpen by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialogOpen) {
|
||||||
|
ShowQRDialog(
|
||||||
|
user,
|
||||||
|
onScan = {
|
||||||
|
dialogOpen = false
|
||||||
|
navController.navigate(it)
|
||||||
|
},
|
||||||
|
onClose = { dialogOpen = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(25.dp),
|
||||||
|
onClick = { dialogOpen = true }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_qrcode),
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(15.dp),
|
||||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -51,6 +52,9 @@ fun LoginPage(
|
|||||||
var termsAcceptanceIsRequired by remember { mutableStateOf("") }
|
var termsAcceptanceIsRequired by remember { mutableStateOf("") }
|
||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
var dialogOpen by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -117,16 +121,36 @@ fun LoginPage(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(onClick = { showPassword = !showPassword }) {
|
Row {
|
||||||
Icon(
|
IconButton(onClick = { showPassword = !showPassword }) {
|
||||||
imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
Icon(
|
||||||
contentDescription = if (showPassword) {
|
imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||||
stringResource(R.string.show_password)
|
contentDescription = if (showPassword) {
|
||||||
} else {
|
stringResource(R.string.show_password)
|
||||||
stringResource(
|
} else {
|
||||||
R.string.hide_password
|
stringResource(
|
||||||
)
|
R.string.hide_password
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
if (dialogOpen) {
|
||||||
|
SimpleQrCodeScanner {
|
||||||
|
dialogOpen = false
|
||||||
|
if (!it.isNullOrEmpty()) {
|
||||||
|
key.value = TextFieldValue(it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(onClick = { dialogOpen = true }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_qrcode),
|
||||||
|
null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = MaterialTheme.colors.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user