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 144
|
versionCode 150
|
||||||
versionName "0.42.2"
|
versionName "0.43.2"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
|
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
|
||||||
import com.vitorpamplona.amethyst.ui.actions.ImgurServer
|
import com.vitorpamplona.amethyst.ui.actions.ImgurServer
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NostrBuildServer
|
import com.vitorpamplona.amethyst.ui.actions.NostrBuildServer
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.NostrFilesDevServer
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NostrImgServer
|
import com.vitorpamplona.amethyst.ui.actions.NostrImgServer
|
||||||
import junit.framework.TestCase.assertNotNull
|
import junit.framework.TestCase.assertNotNull
|
||||||
import junit.framework.TestCase.fail
|
import junit.framework.TestCase.fail
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
@@ -21,12 +21,12 @@ class ImageUploadTesting {
|
|||||||
|
|
||||||
@Test()
|
@Test()
|
||||||
fun testImgurUpload() = runBlocking {
|
fun testImgurUpload() = runBlocking {
|
||||||
val inputStream = Base64.getDecoder().decode(image).inputStream()
|
val bytes = Base64.getDecoder().decode(image)
|
||||||
|
val inputStream = bytes.inputStream()
|
||||||
println("Uploading")
|
|
||||||
|
|
||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
inputStream,
|
inputStream,
|
||||||
|
bytes.size.toLong(),
|
||||||
"image/gif",
|
"image/gif",
|
||||||
ImgurServer(),
|
ImgurServer(),
|
||||||
onSuccess = { url, contentType ->
|
onSuccess = { url, contentType ->
|
||||||
@@ -39,18 +39,17 @@ class ImageUploadTesting {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
delay(1000)
|
delay(5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test()
|
@Test()
|
||||||
@Ignore
|
|
||||||
fun testNostrBuildUpload() = runBlocking {
|
fun testNostrBuildUpload() = runBlocking {
|
||||||
val inputStream = Base64.getDecoder().decode(image).inputStream()
|
val bytes = Base64.getDecoder().decode(image)
|
||||||
|
val inputStream = bytes.inputStream()
|
||||||
println("Uploading")
|
|
||||||
|
|
||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
inputStream,
|
inputStream,
|
||||||
|
bytes.size.toLong(),
|
||||||
"image/gif",
|
"image/gif",
|
||||||
NostrBuildServer(),
|
NostrBuildServer(),
|
||||||
onSuccess = { url, contentType ->
|
onSuccess = { url, contentType ->
|
||||||
@@ -68,12 +67,12 @@ class ImageUploadTesting {
|
|||||||
|
|
||||||
@Test()
|
@Test()
|
||||||
fun testNostrImgUpload() = runBlocking {
|
fun testNostrImgUpload() = runBlocking {
|
||||||
val inputStream = Base64.getDecoder().decode(image).inputStream()
|
val bytes = Base64.getDecoder().decode(image)
|
||||||
|
val inputStream = bytes.inputStream()
|
||||||
println("Uploading")
|
|
||||||
|
|
||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
inputStream,
|
inputStream,
|
||||||
|
bytes.size.toLong(),
|
||||||
"image/gif",
|
"image/gif",
|
||||||
NostrImgServer(),
|
NostrImgServer(),
|
||||||
onSuccess = { url, contentType ->
|
onSuccess = { url, contentType ->
|
||||||
@@ -88,4 +87,27 @@ class ImageUploadTesting {
|
|||||||
|
|
||||||
delay(1000)
|
delay(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun testNostrFilesDevUpload() = runBlocking {
|
||||||
|
val bytes = Base64.getDecoder().decode(image)
|
||||||
|
val inputStream = bytes.inputStream()
|
||||||
|
|
||||||
|
ImageUploader.uploadImage(
|
||||||
|
inputStream,
|
||||||
|
bytes.size.toLong(),
|
||||||
|
"image/gif",
|
||||||
|
NostrFilesDevServer(),
|
||||||
|
onSuccess = { url, contentType ->
|
||||||
|
println("Uploaded to $url")
|
||||||
|
assertNotNull(url)
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
println("Failed to Upload")
|
||||||
|
fail("${it.message}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
delay(5000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class PrivateZapTests {
|
|||||||
if (recepientPK != null && recepientPost != null) {
|
if (recepientPK != null && recepientPost != null) {
|
||||||
val privateKey = createEncryptionPrivateKey(loggedIn.toHexKey(), recepientPost, privateZapRequest.createdAt)
|
val privateKey = createEncryptionPrivateKey(loggedIn.toHexKey(), recepientPost, privateZapRequest.createdAt)
|
||||||
val decodedPrivateZap =
|
val decodedPrivateZap =
|
||||||
LnZapRequestEvent.checkForPrivateZap(privateZapRequest, privateKey, recepientPK)
|
privateZapRequest.getPrivateZapEvent(privateKey, recepientPK)
|
||||||
|
|
||||||
println(decodedPrivateZap?.toJson())
|
println(decodedPrivateZap?.toJson())
|
||||||
assertNotNull(decodedPrivateZap)
|
assertNotNull(decodedPrivateZap)
|
||||||
@@ -127,8 +127,7 @@ class PrivateZapTests {
|
|||||||
|
|
||||||
if (recepientPK != null && recepientPost != null) {
|
if (recepientPK != null && recepientPost != null) {
|
||||||
val privateKey = createEncryptionPrivateKey(loggedIn.toHexKey(), recepientPost, privateZapRequest.createdAt)
|
val privateKey = createEncryptionPrivateKey(loggedIn.toHexKey(), recepientPost, privateZapRequest.createdAt)
|
||||||
val decodedPrivateZap =
|
val decodedPrivateZap = privateZapRequest.getPrivateZapEvent(privateKey, recepientPK)
|
||||||
LnZapRequestEvent.checkForPrivateZap(privateZapRequest, privateKey, recepientPK)
|
|
||||||
|
|
||||||
println(decodedPrivateZap?.toJson())
|
println(decodedPrivateZap?.toJson())
|
||||||
assertNotNull(decodedPrivateZap)
|
assertNotNull(decodedPrivateZap)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.hexToByteArray
|
||||||
import com.vitorpamplona.amethyst.service.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClient
|
||||||
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
@@ -284,7 +284,7 @@ object LocalPreferences {
|
|||||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||||
|
|
||||||
val a = Account(
|
val a = Account(
|
||||||
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
|
Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
||||||
followingChannels,
|
followingChannels,
|
||||||
hiddenUsers,
|
hiddenUsers,
|
||||||
localRelays,
|
localRelays,
|
||||||
|
|||||||
@@ -181,44 +181,49 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isNIP47Author(pubkeyHex: String?): Boolean {
|
fun isNIP47Author(pubkeyHex: String?): Boolean {
|
||||||
val privKey = zapPaymentRequest?.secret?.toByteArray() ?: loggedIn.privKey!!
|
val privKey = zapPaymentRequest?.secret?.hexToByteArray() ?: loggedIn.privKey
|
||||||
|
|
||||||
|
if (privKey == null) return false
|
||||||
|
|
||||||
val pubKey = Utils.pubkeyCreate(privKey).toHexKey()
|
val pubKey = Utils.pubkeyCreate(privKey).toHexKey()
|
||||||
return (pubKey == pubkeyHex)
|
return (pubKey == pubkeyHex)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decryptZapPaymentResponseEvent(zapResponseEvent: LnZapPaymentResponseEvent): Response? {
|
fun decryptZapPaymentResponseEvent(zapResponseEvent: LnZapPaymentResponseEvent): Response? {
|
||||||
val myNip47 = zapPaymentRequest ?: return null
|
val myNip47 = zapPaymentRequest ?: return null
|
||||||
return zapResponseEvent.response(
|
|
||||||
myNip47.secret?.toByteArray() ?: loggedIn.privKey!!,
|
val privKey = myNip47.secret?.hexToByteArray() ?: loggedIn.privKey
|
||||||
myNip47.pubKeyHex.toByteArray()
|
val pubKey = myNip47.pubKeyHex.hexToByteArray()
|
||||||
)
|
|
||||||
|
if (privKey == null) return null
|
||||||
|
|
||||||
|
return zapResponseEvent.response(privKey, pubKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun calculateIfNoteWasZappedByAccount(zappedNote: Note): Boolean {
|
fun calculateIfNoteWasZappedByAccount(zappedNote: Note?): Boolean {
|
||||||
return zappedNote.isZappedBy(userProfile(), this) == true
|
return zappedNote?.isZappedBy(userProfile(), this) == true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun calculateZappedAmount(zappedNote: Note?): BigDecimal {
|
fun calculateZappedAmount(zappedNote: Note?): BigDecimal {
|
||||||
return zappedNote?.zappedAmount(
|
val privKey = zapPaymentRequest?.secret?.hexToByteArray() ?: loggedIn.privKey
|
||||||
zapPaymentRequest?.secret?.toByteArray() ?: loggedIn.privKey!!,
|
val pubKey = zapPaymentRequest?.pubKeyHex?.hexToByteArray()
|
||||||
zapPaymentRequest?.pubKeyHex?.toByteArray()
|
return zappedNote?.zappedAmount(privKey, pubKey) ?: BigDecimal.ZERO
|
||||||
) ?: BigDecimal.ZERO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendZapPaymentRequestFor(bolt11: String, zappedNote: Note?, onResponse: (Response?) -> Unit) {
|
fun sendZapPaymentRequestFor(bolt11: String, zappedNote: Note?, onResponse: (Response?) -> Unit) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
zapPaymentRequest?.let { nip47 ->
|
zapPaymentRequest?.let { nip47 ->
|
||||||
val event = LnZapPaymentRequestEvent.create(bolt11, nip47.pubKeyHex, nip47.secret?.toByteArray() ?: loggedIn.privKey!!)
|
val event = LnZapPaymentRequestEvent.create(bolt11, nip47.pubKeyHex, nip47.secret?.hexToByteArray() ?: loggedIn.privKey!!)
|
||||||
|
|
||||||
val wcListener = NostrLnZapPaymentResponseDataSource(nip47.pubKeyHex, event.pubKey, event.id)
|
val wcListener = NostrLnZapPaymentResponseDataSource(nip47.pubKeyHex, event.pubKey, event.id)
|
||||||
wcListener.start()
|
wcListener.start()
|
||||||
|
|
||||||
LocalCache.consume(event, zappedNote) {
|
LocalCache.consume(event, zappedNote) {
|
||||||
// After the response is received.
|
// After the response is received.
|
||||||
val privKey = nip47.secret?.toByteArray()
|
val privKey = nip47.secret?.hexToByteArray()
|
||||||
if (privKey != null) {
|
if (privKey != null) {
|
||||||
onResponse(it.response(privKey, nip47.pubKeyHex.toByteArray()))
|
onResponse(it.response(privKey, nip47.pubKeyHex.hexToByteArray()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,13 +823,13 @@ class Account(
|
|||||||
return if (event is PrivateDmEvent && loggedIn.privKey != null) {
|
return if (event is PrivateDmEvent && loggedIn.privKey != null) {
|
||||||
var pubkeyToUse = event.pubKey
|
var pubkeyToUse = event.pubKey
|
||||||
|
|
||||||
val recepientPK = event.recipientPubKey()
|
val recepientPK = event.verifiedRecipientPubKey()
|
||||||
|
|
||||||
if (note.author == userProfile() && recepientPK != null) {
|
if (note.author == userProfile() && recepientPK != null) {
|
||||||
pubkeyToUse = recepientPK
|
pubkeyToUse = recepientPK
|
||||||
}
|
}
|
||||||
|
|
||||||
event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray())
|
event.plainContent(loggedIn.privKey!!, pubkeyToUse.hexToByteArray())
|
||||||
} else if (event is LnZapRequestEvent && loggedIn.privKey != null) {
|
} else if (event is LnZapRequestEvent && loggedIn.privKey != null) {
|
||||||
decryptZapContentAuthor(note)?.content()
|
decryptZapContentAuthor(note)?.content()
|
||||||
} else {
|
} else {
|
||||||
@@ -845,7 +850,7 @@ class Account(
|
|||||||
val privateKeyToUse = loggedInPrivateKey
|
val privateKeyToUse = loggedInPrivateKey
|
||||||
val pubkeyToUse = event.pubKey
|
val pubkeyToUse = event.pubKey
|
||||||
|
|
||||||
LnZapRequestEvent.checkForPrivateZap(event, privateKeyToUse, pubkeyToUse)
|
event.getPrivateZapEvent(privateKeyToUse, pubkeyToUse)
|
||||||
} else {
|
} else {
|
||||||
// if the sender is logged in, these are the params
|
// if the sender is logged in, these are the params
|
||||||
val altPubkeyToUse = recipientPK
|
val altPubkeyToUse = recipientPK
|
||||||
@@ -866,14 +871,24 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (altPrivateKeyToUse != null && altPubkeyToUse != null) {
|
if (altPrivateKeyToUse != null && altPubkeyToUse != null) {
|
||||||
val result = LnZapRequestEvent.checkForPrivateZap(event, altPrivateKeyToUse, altPubkeyToUse)
|
val altPubKeyFromPrivate = Utils.pubkeyCreate(altPrivateKeyToUse).toHexKey()
|
||||||
|
|
||||||
|
if (altPubKeyFromPrivate == event.pubKey) {
|
||||||
|
val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse)
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
Log.w("Private ZAP Decrypt", "Fail to decrypt Zap from ${note.author?.toBestDisplayName()} ${note.idNote()}")
|
Log.w(
|
||||||
|
"Private ZAP Decrypt",
|
||||||
|
"Fail to decrypt Zap from ${note.author?.toBestDisplayName()} ${note.idNote()}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ fun ByteArray.toHexKey(): HexKey {
|
|||||||
return toHex()
|
return toHex()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun HexKey.toByteArray(): ByteArray {
|
fun HexKey.hexToByteArray(): ByteArray {
|
||||||
return Hex.decode(this)
|
return Hex.decode(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ fun HexKey.toDisplayHexKey(): String {
|
|||||||
|
|
||||||
fun decodePublicKey(key: String): ByteArray {
|
fun decodePublicKey(key: String): ByteArray {
|
||||||
val parsed = Nip19.uriToRoute(key)
|
val parsed = Nip19.uriToRoute(key)
|
||||||
val pubKeyParsed = parsed?.hex?.toByteArray()
|
val pubKeyParsed = parsed?.hex?.hexToByteArray()
|
||||||
|
|
||||||
return if (key.startsWith("nsec")) {
|
return if (key.startsWith("nsec")) {
|
||||||
Persona(privKey = key.bechToBytes()).pubKey
|
Persona(privKey = key.bechToBytes()).pubKey
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ object LocalCache {
|
|||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val recipient = event.recipientPubKey()?.let { getOrCreateUser(it) }
|
val recipient = event.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }
|
||||||
|
|
||||||
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
||||||
|
|
||||||
@@ -407,7 +407,7 @@ object LocalCache {
|
|||||||
|
|
||||||
if (deleteNote.event is PrivateDmEvent) {
|
if (deleteNote.event is PrivateDmEvent) {
|
||||||
val author = deleteNote.author
|
val author = deleteNote.author
|
||||||
val recipient = (deleteNote.event as? PrivateDmEvent)?.recipientPubKey()?.let { checkGetOrCreateUser(it) }
|
val recipient = (deleteNote.event as? PrivateDmEvent)?.verifiedRecipientPubKey()?.let { checkGetOrCreateUser(it) }
|
||||||
|
|
||||||
if (recipient != null && author != null) {
|
if (recipient != null && author != null) {
|
||||||
author.removeMessage(recipient, deleteNote)
|
author.removeMessage(recipient, deleteNote)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import kotlin.time.measureTimedValue
|
|||||||
|
|
||||||
class ThreadAssembler {
|
class ThreadAssembler {
|
||||||
|
|
||||||
fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? {
|
private fun searchRoot(note: Note, testedNotes: MutableSet<Note> = mutableSetOf()): Note? {
|
||||||
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
|
if (note.replyTo == null || note.replyTo?.isEmpty() == true) return note
|
||||||
|
|
||||||
testedNotes.add(note)
|
testedNotes.add(note)
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class User(val pubkeyHex: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun getOrCreatePrivateChatroom(user: User): Chatroom {
|
private fun getOrCreatePrivateChatroom(user: User): Chatroom {
|
||||||
return privateChatrooms[user] ?: run {
|
return privateChatrooms[user] ?: run {
|
||||||
val privateChatroom = Chatroom(setOf<Note>())
|
val privateChatroom = Chatroom(setOf<Note>())
|
||||||
privateChatrooms = privateChatrooms + Pair(user, privateChatroom)
|
privateChatrooms = privateChatrooms + Pair(user, privateChatroom)
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ object BlurHashRequester {
|
|||||||
.Builder(context)
|
.Builder(context)
|
||||||
.data("bluehash:$encodedMessage")
|
.data("bluehash:$encodedMessage")
|
||||||
.fetcherFactory(BlurHashFetcher.Factory)
|
.fetcherFactory(BlurHashFetcher.Factory)
|
||||||
.crossfade(100)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
|
||||||
import io.trbl.blurhash.BlurHash
|
import io.trbl.blurhash.BlurHash
|
||||||
import java.net.URL
|
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -19,11 +19,15 @@ class FileHeader(
|
|||||||
val description: String? = null
|
val description: String? = null
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun prepare(fileUrl: String, mimeType: String?, description: String?, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
suspend fun prepare(fileUrl: String, mimeType: String?, description: String?, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
||||||
try {
|
try {
|
||||||
val imageData = URL(fileUrl).readBytes()
|
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
|
||||||
|
|
||||||
|
if (imageData != null) {
|
||||||
prepare(imageData, fileUrl, mimeType, description, onReady, onError)
|
prepare(imageData, fileUrl, mimeType, description, onReady, onError)
|
||||||
|
} else {
|
||||||
|
onError()
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
|
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
|
||||||
onError()
|
onError()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.vitorpamplona.amethyst.service
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||||
@@ -10,13 +9,8 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
|||||||
object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(userId: String?) {
|
fun loadUserProfile(user: User?) {
|
||||||
if (userId != null) {
|
this.user = user
|
||||||
user = LocalCache.getOrCreateUser(userId)
|
|
||||||
} else {
|
|
||||||
user = null
|
|
||||||
}
|
|
||||||
|
|
||||||
resetFilters()
|
resetFilters()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.hexToByteArray
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Tlv
|
import com.vitorpamplona.amethyst.service.nip19.Tlv
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
@@ -14,7 +14,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela
|
|||||||
|
|
||||||
fun toNAddr(): String {
|
fun toNAddr(): String {
|
||||||
val kind = kind.toByteArray()
|
val kind = kind.toByteArray()
|
||||||
val author = pubKeyHex.toByteArray()
|
val author = pubKeyHex.hexToByteArray()
|
||||||
val dTag = dTag.toByteArray(Charsets.UTF_8)
|
val dTag = dTag.toByteArray(Charsets.UTF_8)
|
||||||
val relay = relay?.toByteArray(Charsets.UTF_8)
|
val relay = relay?.toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,9 @@ class BadgeAwardEvent(
|
|||||||
content: String,
|
content: String,
|
||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun awardees() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
fun awardees() = taggedUsers()
|
||||||
fun awardDefinition() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
|
|
||||||
val aTagValue = it.getOrNull(1)
|
|
||||||
val relay = it.getOrNull(2)
|
|
||||||
|
|
||||||
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
fun awardDefinition() = taggedAddresses()
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 8
|
const val kind = 8
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ class BadgeDefinitionEvent(
|
|||||||
content: String,
|
content: String,
|
||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
|
fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||||
fun address() = ATag(kind, pubKey, dTag(), null)
|
fun address() = ATag(kind, pubKey, dTag(), null)
|
||||||
|
|
||||||
fun name() = tags.filter { it.firstOrNull() == "name" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1)
|
||||||
fun thumb() = tags.filter { it.firstOrNull() == "thumb" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun thumb() = tags.firstOrNull { it.size > 1 && it[0] == "thumb" }?.get(1)
|
||||||
fun image() = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
|
||||||
fun description() = tags.filter { it.firstOrNull() == "description" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 30009
|
const val kind = 30009
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.tagSearch
|
import com.vitorpamplona.amethyst.model.tagSearch
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
@@ -41,17 +42,18 @@ open class BaseTextNoteEvent(
|
|||||||
val key = matcher2.group(3) // bech32
|
val key = matcher2.group(3) // bech32
|
||||||
val additionalChars = matcher2.group(4) // additional chars
|
val additionalChars = matcher2.group(4) // additional chars
|
||||||
|
|
||||||
|
try {
|
||||||
val parsed = Nip19.parseComponents(uriScheme, type, key, additionalChars)
|
val parsed = Nip19.parseComponents(uriScheme, type, key, additionalChars)
|
||||||
|
|
||||||
if (parsed != null) {
|
if (parsed != null) {
|
||||||
try {
|
|
||||||
val tag = tags.firstOrNull { it.size > 1 && it[1] == parsed.hex }
|
val tag = tags.firstOrNull { it.size > 1 && it[1] == parsed.hex }
|
||||||
|
|
||||||
if (tag != null && tag[0] == "p") {
|
if (tag != null && tag[0] == "p") {
|
||||||
returningList.add(tag[1])
|
returningList.add(tag[1])
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("Unable to parse cited users that matched a NIP19 regex", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ open class Event(
|
|||||||
|
|
||||||
override fun toJson(): String = gson.toJson(this)
|
override fun toJson(): String = gson.toJson(this)
|
||||||
|
|
||||||
|
fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" }
|
||||||
|
|
||||||
fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
|
fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
|
||||||
fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
|
fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
|
||||||
|
|
||||||
@@ -83,7 +85,7 @@ open class Event(
|
|||||||
|
|
||||||
override fun getReward(): BigDecimal? {
|
override fun getReward(): BigDecimal? {
|
||||||
return try {
|
return try {
|
||||||
tags.filter { it.firstOrNull() == "reward" }.mapNotNull { it.getOrNull(1)?.let { BigDecimal(it) } }.firstOrNull()
|
tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service.model
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.hexToByteArray
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
|
|
||||||
abstract class GeneralListEvent(
|
abstract class GeneralListEvent(
|
||||||
@@ -24,7 +24,7 @@ abstract class GeneralListEvent(
|
|||||||
|
|
||||||
fun plainContent(privKey: ByteArray): String? {
|
fun plainContent(privKey: ByteArray): String? {
|
||||||
return try {
|
return try {
|
||||||
val sharedSecret = Utils.getSharedSecret(privKey, pubKey.toByteArray())
|
val sharedSecret = Utils.getSharedSecret(privKey, pubKey.hexToByteArray())
|
||||||
|
|
||||||
return Utils.decrypt(content, sharedSecret)
|
return Utils.decrypt(content, sharedSecret)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
+13
-3
@@ -6,7 +6,7 @@ import com.google.gson.JsonDeserializer
|
|||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonParseException
|
import com.google.gson.JsonParseException
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.hexToByteArray
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
@@ -21,9 +21,17 @@ class LnZapPaymentRequestEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
// Once one of an app user decrypts the payment, all users else can see it.
|
||||||
|
@Transient
|
||||||
|
private var lnInvoice: String? = null
|
||||||
|
|
||||||
fun walletServicePubKey() = tags.firstOrNull() { it.size > 1 && it[0] == "p" }?.get(1)
|
fun walletServicePubKey() = tags.firstOrNull() { it.size > 1 && it[0] == "p" }?.get(1)
|
||||||
|
|
||||||
fun lnInvoice(privKey: ByteArray, pubkey: ByteArray): String? {
|
fun lnInvoice(privKey: ByteArray, pubkey: ByteArray): String? {
|
||||||
|
if (lnInvoice != null) {
|
||||||
|
return lnInvoice
|
||||||
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val sharedSecret = Utils.getSharedSecret(privKey, pubkey)
|
val sharedSecret = Utils.getSharedSecret(privKey, pubkey)
|
||||||
|
|
||||||
@@ -31,7 +39,9 @@ class LnZapPaymentRequestEvent(
|
|||||||
|
|
||||||
val payInvoiceMethod = gson.fromJson(jsonText, Request::class.java)
|
val payInvoiceMethod = gson.fromJson(jsonText, Request::class.java)
|
||||||
|
|
||||||
return (payInvoiceMethod as? PayInvoiceMethod)?.params?.invoice
|
lnInvoice = (payInvoiceMethod as? PayInvoiceMethod)?.params?.invoice
|
||||||
|
|
||||||
|
return lnInvoice
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w("BookmarkList", "Error decrypting the message ${e.message}")
|
Log.w("BookmarkList", "Error decrypting the message ${e.message}")
|
||||||
null
|
null
|
||||||
@@ -53,7 +63,7 @@ class LnZapPaymentRequestEvent(
|
|||||||
val content = Utils.encrypt(
|
val content = Utils.encrypt(
|
||||||
serializedRequest,
|
serializedRequest,
|
||||||
privateKey,
|
privateKey,
|
||||||
walletServicePubkey.toByteArray()
|
walletServicePubkey.hexToByteArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
val tags = mutableListOf<List<String>>()
|
val tags = mutableListOf<List<String>>()
|
||||||
|
|||||||
+12
-3
@@ -19,10 +19,14 @@ class LnZapPaymentResponseEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
// Once one of an app user decrypts the payment, all users else can see it.
|
||||||
|
@Transient
|
||||||
|
private var response: Response? = null
|
||||||
|
|
||||||
fun requestAuthor() = tags.firstOrNull() { it.size > 1 && it[0] == "p" }?.get(1)
|
fun requestAuthor() = tags.firstOrNull() { it.size > 1 && it[0] == "p" }?.get(1)
|
||||||
fun requestId() = tags.firstOrNull() { it.size > 1 && it[0] == "e" }?.get(1)
|
fun requestId() = tags.firstOrNull() { it.size > 1 && it[0] == "e" }?.get(1)
|
||||||
|
|
||||||
fun decrypt(privKey: ByteArray, pubKey: ByteArray): String? {
|
private fun decrypt(privKey: ByteArray, pubKey: ByteArray): String? {
|
||||||
return try {
|
return try {
|
||||||
val sharedSecret = Utils.getSharedSecret(privKey, pubKey)
|
val sharedSecret = Utils.getSharedSecret(privKey, pubKey)
|
||||||
|
|
||||||
@@ -39,10 +43,14 @@ class LnZapPaymentResponseEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun response(privKey: ByteArray, pubKey: ByteArray): Response? = try {
|
fun response(privKey: ByteArray, pubKey: ByteArray): Response? {
|
||||||
|
if (response != null) response
|
||||||
|
|
||||||
|
return try {
|
||||||
if (content.isNotEmpty()) {
|
if (content.isNotEmpty()) {
|
||||||
val decrypted = decrypt(privKey, pubKey)
|
val decrypted = decrypt(privKey, pubKey)
|
||||||
gson.fromJson(decrypted, Response::class.java)
|
response = gson.fromJson(decrypted, Response::class.java)
|
||||||
|
response
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -50,6 +58,7 @@ class LnZapPaymentResponseEvent(
|
|||||||
Log.w("LnZapPaymentResponseEvent", "Can't parse content as a payment response: $content", e)
|
Log.w("LnZapPaymentResponseEvent", "Can't parse content as a payment response: $content", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 23195
|
const val kind = 23195
|
||||||
|
|||||||
@@ -20,12 +20,37 @@ class LnZapRequestEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private var privateZapEvent: Event? = null
|
||||||
|
|
||||||
fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
|
fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
|
||||||
|
|
||||||
fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
|
fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
|
||||||
|
|
||||||
fun isPrivateZap() = tags.any { t -> t.size >= 2 && t[0] == "anon" && t[1].isNotBlank() }
|
fun isPrivateZap() = tags.any { t -> t.size >= 2 && t[0] == "anon" && t[1].isNotBlank() }
|
||||||
|
|
||||||
|
fun getPrivateZapEvent(loggedInUserPrivKey: ByteArray, pubKey: HexKey): Event? {
|
||||||
|
if (privateZapEvent != null) return privateZapEvent
|
||||||
|
|
||||||
|
val anonTag = tags.firstOrNull { t -> t.size >= 2 && t[0] == "anon" }
|
||||||
|
if (anonTag != null) {
|
||||||
|
val encnote = anonTag[1]
|
||||||
|
if (encnote.isNotBlank()) {
|
||||||
|
try {
|
||||||
|
val note = decryptPrivateZapMessage(encnote, loggedInUserPrivKey, pubKey.hexToByteArray())
|
||||||
|
val decryptedEvent = fromJson(note)
|
||||||
|
if (decryptedEvent.kind == 9733) {
|
||||||
|
privateZapEvent = decryptedEvent
|
||||||
|
return privateZapEvent
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 9734
|
const val kind = 9734
|
||||||
|
|
||||||
@@ -59,7 +84,7 @@ class LnZapRequestEvent(
|
|||||||
} else if (zapType == LnZapEvent.ZapType.PRIVATE) {
|
} else if (zapType == LnZapEvent.ZapType.PRIVATE) {
|
||||||
var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), originalNote.id(), createdAt)
|
var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), originalNote.id(), createdAt)
|
||||||
var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson()
|
var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson()
|
||||||
var encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, originalNote.pubKey().toByteArray())
|
var encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, originalNote.pubKey().hexToByteArray())
|
||||||
tags = tags + listOf(listOf("anon", encryptedContent))
|
tags = tags + listOf(listOf("anon", encryptedContent))
|
||||||
content = "" // make sure public content is empty, as the content is encrypted
|
content = "" // make sure public content is empty, as the content is encrypted
|
||||||
privkey = encryptionPrivateKey // sign event with generated privkey
|
privkey = encryptionPrivateKey // sign event with generated privkey
|
||||||
@@ -92,7 +117,7 @@ class LnZapRequestEvent(
|
|||||||
} else if (zapType == LnZapEvent.ZapType.PRIVATE) {
|
} else if (zapType == LnZapEvent.ZapType.PRIVATE) {
|
||||||
var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), userHex, createdAt)
|
var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), userHex, createdAt)
|
||||||
var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson()
|
var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson()
|
||||||
var encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, userHex.toByteArray())
|
var encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, userHex.hexToByteArray())
|
||||||
tags = tags + listOf(listOf("anon", encryptedContent))
|
tags = tags + listOf(listOf("anon", encryptedContent))
|
||||||
content = ""
|
content = ""
|
||||||
privkey = encryptionPrivateKey
|
privkey = encryptionPrivateKey
|
||||||
@@ -109,7 +134,7 @@ class LnZapRequestEvent(
|
|||||||
return sha256.digest(strbyte)
|
return sha256.digest(strbyte)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String {
|
private fun encryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String {
|
||||||
var sharedSecret = Utils.getSharedSecret(privkey, pubkey)
|
var sharedSecret = Utils.getSharedSecret(privkey, pubkey)
|
||||||
val iv = ByteArray(16)
|
val iv = ByteArray(16)
|
||||||
SecureRandom().nextBytes(iv)
|
SecureRandom().nextBytes(iv)
|
||||||
@@ -128,7 +153,7 @@ class LnZapRequestEvent(
|
|||||||
return encryptedMsgBech32 + "_" + ivBech32
|
return encryptedMsgBech32 + "_" + ivBech32
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String {
|
private fun decryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String {
|
||||||
var sharedSecret = Utils.getSharedSecret(privkey, pubkey)
|
var sharedSecret = Utils.getSharedSecret(privkey, pubkey)
|
||||||
if (sharedSecret.size != 16 && sharedSecret.size != 32) {
|
if (sharedSecret.size != 16 && sharedSecret.size != 32) {
|
||||||
throw IllegalArgumentException("Invalid shared secret size")
|
throw IllegalArgumentException("Invalid shared secret size")
|
||||||
@@ -150,61 +175,5 @@ class LnZapRequestEvent(
|
|||||||
throw IllegalArgumentException("Bad padding: ${ex.message}")
|
throw IllegalArgumentException("Bad padding: ${ex.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkForPrivateZap(zapRequest: LnZapRequestEvent, loggedInUserPrivKey: ByteArray, pubKey: HexKey): Event? {
|
|
||||||
val anonTag = zapRequest.tags.firstOrNull { t -> t.size >= 2 && t[0] == "anon" }
|
|
||||||
if (anonTag != null) {
|
|
||||||
val encnote = anonTag[1]
|
|
||||||
if (encnote.isNotBlank()) {
|
|
||||||
try {
|
|
||||||
val note = decryptPrivateZapMessage(encnote, loggedInUserPrivKey, pubKey.toByteArray())
|
|
||||||
val decryptedEvent = fromJson(note)
|
|
||||||
if (decryptedEvent.kind == 9733) {
|
|
||||||
return decryptedEvent
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
{
|
|
||||||
"pubkey": "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
|
|
||||||
"content": "",
|
|
||||||
"id": "d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d",
|
|
||||||
"created_at": 1674164539,
|
|
||||||
"sig": "77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d",
|
|
||||||
"kind": 9734,
|
|
||||||
"tags": [
|
|
||||||
[
|
|
||||||
"e",
|
|
||||||
"3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"p",
|
|
||||||
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"relays",
|
|
||||||
"wss://relay.damus.io",
|
|
||||||
"wss://nostr-relay.wlvs.space",
|
|
||||||
"wss://nostr.fmt.wiz.biz",
|
|
||||||
"wss://relay.nostr.bg",
|
|
||||||
"wss://nostr.oxtr.dev",
|
|
||||||
"wss://nostr.v0l.io",
|
|
||||||
"wss://brb.io",
|
|
||||||
"wss://nostr.bitcoiner.social",
|
|
||||||
"ws://monad.jb55.com:8080",
|
|
||||||
"wss://relay.snort.social"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"poll_option", "n"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"ots": <base64-encoded OTS file data> // TODO
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service.model
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.hexToByteArray
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -21,7 +21,7 @@ class MuteListEvent(
|
|||||||
|
|
||||||
fun plainContent(privKey: ByteArray): String? {
|
fun plainContent(privKey: ByteArray): String? {
|
||||||
return try {
|
return try {
|
||||||
val sharedSecret = Utils.getSharedSecret(privKey, pubKey.toByteArray())
|
val sharedSecret = Utils.getSharedSecret(privKey, pubKey.hexToByteArray())
|
||||||
|
|
||||||
return Utils.decrypt(content, sharedSecret)
|
return Utils.decrypt(content, sharedSecret)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ class PrivateDmEvent(
|
|||||||
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used
|
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used
|
||||||
* for initial messages.
|
* for initial messages.
|
||||||
*/
|
*/
|
||||||
fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.run { Hex.decode(this[1]).toHexKey() } // makes sure its a valid one
|
fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }
|
||||||
|
|
||||||
|
fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this[1]) }?.getOrNull()
|
||||||
|
|
||||||
|
fun verifiedRecipientPubKey() = recipientPubKey()?.runCatching { Hex.decode(this[1]).toHexKey() }?.getOrNull() // makes sure its a valid one
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To be fully compatible with nip-04, we read e-tags that are in violation to nip-18.
|
* To be fully compatible with nip-04, we read e-tags that are in violation to nip-18.
|
||||||
@@ -31,6 +35,11 @@ class PrivateDmEvent(
|
|||||||
*/
|
*/
|
||||||
fun replyTo() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
fun replyTo() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||||
|
|
||||||
|
fun with(pubkeyHex: String): Boolean {
|
||||||
|
return pubkeyHex == pubKey ||
|
||||||
|
tags.firstOrNull { it.size > 1 && it[0] == "p" }?.getOrNull(1) == pubkeyHex
|
||||||
|
}
|
||||||
|
|
||||||
fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? {
|
fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? {
|
||||||
return try {
|
return try {
|
||||||
val sharedSecret = Utils.getSharedSecret(privKey, pubKey)
|
val sharedSecret = Utils.getSharedSecret(privKey, pubKey)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.service.nip19
|
package com.vitorpamplona.amethyst.service.nip19
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.hexToByteArray
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import nostr.postr.Bech32
|
import nostr.postr.Bech32
|
||||||
import nostr.postr.bechToBytes
|
import nostr.postr.bechToBytes
|
||||||
@@ -147,8 +147,8 @@ object Nip19 {
|
|||||||
|
|
||||||
public fun createNEvent(idHex: String, author: String?, kind: Int?, relay: String?): String {
|
public fun createNEvent(idHex: String, author: String?, kind: Int?, relay: String?): String {
|
||||||
val kind = kind?.toByteArray()
|
val kind = kind?.toByteArray()
|
||||||
val author = author?.toByteArray()
|
val author = author?.hexToByteArray()
|
||||||
val idHex = idHex.toByteArray()
|
val idHex = idHex.hexToByteArray()
|
||||||
val relay = relay?.toByteArray(Charsets.UTF_8)
|
val relay = relay?.toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, idHex.size.toByte()) + idHex
|
var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, idHex.size.toByte()) + idHex
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class ImageDownloader {
|
||||||
|
suspend fun waitAndGetImage(imageUrl: String): ByteArray? {
|
||||||
|
var imageData: ByteArray? = null
|
||||||
|
var tentatives = 0
|
||||||
|
|
||||||
|
// Servers are usually not ready.. so tries to download it for 15 times/seconds.
|
||||||
|
while (imageData == null && tentatives < 15) {
|
||||||
|
imageData = try {
|
||||||
|
HttpURLConnection.setFollowRedirects(true)
|
||||||
|
var url = URL(imageUrl)
|
||||||
|
var huc = url.openConnection() as HttpURLConnection
|
||||||
|
huc.instanceFollowRedirects = true
|
||||||
|
var responseCode = huc.responseCode
|
||||||
|
|
||||||
|
if (responseCode in 300..400) {
|
||||||
|
val newUrl: String = huc.getHeaderField("Location")
|
||||||
|
|
||||||
|
// open the new connnection again
|
||||||
|
url = URL(newUrl)
|
||||||
|
huc = url.openConnection() as HttpURLConnection
|
||||||
|
responseCode = huc.responseCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseCode in 200..300) {
|
||||||
|
huc.inputStream.use { it.readBytes() }
|
||||||
|
} else {
|
||||||
|
tentatives++
|
||||||
|
delay(1000)
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
tentatives++
|
||||||
|
delay(1000)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageData
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,16 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.actions
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.service.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClient
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.source
|
import okio.source
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@@ -28,43 +23,51 @@ object ImageUploader {
|
|||||||
fun uploadImage(
|
fun uploadImage(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
server: ServersAvailable,
|
server: ServersAvailable,
|
||||||
context: Context,
|
|
||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
onSuccess: (String, String?) -> Unit,
|
onSuccess: (String, String?) -> Unit,
|
||||||
onError: (Throwable) -> Unit
|
onError: (Throwable) -> Unit
|
||||||
) {
|
) {
|
||||||
val contentType = contentResolver.getType(uri)
|
val contentType = contentResolver.getType(uri)
|
||||||
val imageInputStream = contentResolver.openInputStream(uri)
|
val imageInputStream = contentResolver.openInputStream(uri)
|
||||||
|
|
||||||
|
val length = contentResolver.query(uri, null, null, null, null)?.use {
|
||||||
|
it.moveToFirst()
|
||||||
|
val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)
|
||||||
|
it.getLong(sizeIndex)
|
||||||
|
} ?: 0
|
||||||
|
|
||||||
checkNotNull(imageInputStream) {
|
checkNotNull(imageInputStream) {
|
||||||
"Can't open the image input stream"
|
"Can't open the image input stream"
|
||||||
}
|
}
|
||||||
val myServer = if (server == ServersAvailable.IMGUR) {
|
val myServer = when (server) {
|
||||||
|
ServersAvailable.IMGUR, ServersAvailable.IMGUR_NIP_94 -> {
|
||||||
ImgurServer()
|
ImgurServer()
|
||||||
} else if (server == ServersAvailable.NOSTRIMG) {
|
}
|
||||||
|
ServersAvailable.NOSTRIMG, ServersAvailable.NOSTRIMG_NIP_94 -> {
|
||||||
NostrImgServer()
|
NostrImgServer()
|
||||||
} else if (server == ServersAvailable.NOSTR_BUILD) {
|
}
|
||||||
|
ServersAvailable.NOSTR_BUILD, ServersAvailable.NOSTR_BUILD_NIP_94 -> {
|
||||||
NostrBuildServer()
|
NostrBuildServer()
|
||||||
} else {
|
}
|
||||||
|
ServersAvailable.NOSTRFILES_DEV, ServersAvailable.NOSTRFILES_DEV_NIP_94 -> {
|
||||||
|
NostrFilesDevServer()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
ImgurServer()
|
ImgurServer()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val file = getRealPathFromURI(uri, context)?.let { File(it) } // create path from uri
|
uploadImage(imageInputStream, length, contentType, myServer, onSuccess, onError)
|
||||||
if (file != null) {
|
|
||||||
uploadImage(file, imageInputStream, contentType, myServer, server, onSuccess, onError)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uploadImage(
|
fun uploadImage(
|
||||||
file: File,
|
|
||||||
inputStream: InputStream,
|
inputStream: InputStream,
|
||||||
|
length: Long,
|
||||||
contentType: String?,
|
contentType: String?,
|
||||||
server: FileServer,
|
server: FileServer,
|
||||||
serverType: ServersAvailable,
|
|
||||||
onSuccess: (String, String?) -> Unit,
|
onSuccess: (String, String?) -> Unit,
|
||||||
onError: (Throwable) -> Unit
|
onError: (Throwable) -> Unit
|
||||||
) {
|
) {
|
||||||
val category = contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
|
||||||
|
|
||||||
val fileName = randomChars()
|
val fileName = randomChars()
|
||||||
val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||||
|
|
||||||
@@ -72,27 +75,16 @@ object ImageUploader {
|
|||||||
val requestBody: RequestBody
|
val requestBody: RequestBody
|
||||||
val requestBuilder = Request.Builder()
|
val requestBuilder = Request.Builder()
|
||||||
|
|
||||||
if (serverType == ServersAvailable.NOSTR_BUILD) {
|
|
||||||
requestBuilder.addHeader("Content-Type", "multipart/form-data")
|
|
||||||
requestBody = MultipartBody.Builder()
|
requestBody = MultipartBody.Builder()
|
||||||
.setType(MultipartBody.FORM)
|
.setType(MultipartBody.FORM)
|
||||||
.addFormDataPart(
|
.addFormDataPart(
|
||||||
"fileToUpload",
|
server.inputParameterName(contentType),
|
||||||
"$fileName.$extension",
|
|
||||||
file.asRequestBody(contentType?.toMediaType())
|
|
||||||
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
requestBody = MultipartBody.Builder()
|
|
||||||
.setType(MultipartBody.FORM)
|
|
||||||
.addFormDataPart(
|
|
||||||
category,
|
|
||||||
"$fileName.$extension",
|
"$fileName.$extension",
|
||||||
|
|
||||||
object : RequestBody() {
|
object : RequestBody() {
|
||||||
override fun contentType(): MediaType? =
|
override fun contentType() = contentType?.toMediaType()
|
||||||
contentType?.toMediaType()
|
|
||||||
|
override fun contentLength() = length
|
||||||
|
|
||||||
override fun writeTo(sink: BufferedSink) {
|
override fun writeTo(sink: BufferedSink) {
|
||||||
inputStream.source().use(sink::writeAll)
|
inputStream.source().use(sink::writeAll)
|
||||||
@@ -100,7 +92,6 @@ object ImageUploader {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
|
||||||
|
|
||||||
server.clientID()?.let {
|
server.clientID()?.let {
|
||||||
requestBuilder.addHeader("Authorization", it)
|
requestBuilder.addHeader("Authorization", it)
|
||||||
@@ -117,7 +108,7 @@ object ImageUploader {
|
|||||||
try {
|
try {
|
||||||
check(response.isSuccessful)
|
check(response.isSuccessful)
|
||||||
response.body.use { body ->
|
response.body.use { body ->
|
||||||
val url = server.parseUrlFromSucess(body.string())
|
val url = server.parseUrlFromSuccess(body.string())
|
||||||
checkNotNull(url) {
|
checkNotNull(url) {
|
||||||
"There must be an uploaded image URL in the response"
|
"There must be an uploaded image URL in the response"
|
||||||
}
|
}
|
||||||
@@ -138,41 +129,10 @@ object ImageUploader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRealPathFromURI(uri: Uri, context: Context): String? {
|
|
||||||
val returnCursor = context.contentResolver.query(uri, null, null, null, null)
|
|
||||||
val nameIndex = returnCursor!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
||||||
returnCursor.moveToFirst()
|
|
||||||
val name = returnCursor.getString(nameIndex)
|
|
||||||
val file = File(context.filesDir, name)
|
|
||||||
try {
|
|
||||||
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
|
||||||
val outputStream = FileOutputStream(file)
|
|
||||||
var read = 0
|
|
||||||
val maxBufferSize = 1 * 1024 * 1024
|
|
||||||
val bytesAvailable: Int = inputStream?.available() ?: 0
|
|
||||||
val bufferSize = Math.min(bytesAvailable, maxBufferSize)
|
|
||||||
val buffers = ByteArray(bufferSize)
|
|
||||||
while (inputStream?.read(buffers).also {
|
|
||||||
if (it != null) {
|
|
||||||
read = it
|
|
||||||
}
|
|
||||||
} != -1
|
|
||||||
) {
|
|
||||||
outputStream.write(buffers, 0, read)
|
|
||||||
}
|
|
||||||
Log.e("File Size", "Size " + file.length())
|
|
||||||
inputStream?.close()
|
|
||||||
outputStream.close()
|
|
||||||
Log.e("File Path", "Path " + file.path)
|
|
||||||
} catch (e: java.lang.Exception) {
|
|
||||||
Log.e("Exception", e.message!!)
|
|
||||||
}
|
|
||||||
return file.path
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class FileServer {
|
abstract class FileServer {
|
||||||
abstract fun postUrl(contentType: String?): String
|
abstract fun postUrl(contentType: String?): String
|
||||||
abstract fun parseUrlFromSucess(body: String): String?
|
abstract fun parseUrlFromSuccess(body: String): String?
|
||||||
|
abstract fun inputParameterName(contentType: String?): String
|
||||||
|
|
||||||
open fun clientID(): String? = null
|
open fun clientID(): String? = null
|
||||||
}
|
}
|
||||||
@@ -180,12 +140,16 @@ abstract class FileServer {
|
|||||||
class NostrImgServer : FileServer() {
|
class NostrImgServer : FileServer() {
|
||||||
override fun postUrl(contentType: String?) = "https://nostrimg.com/api/upload"
|
override fun postUrl(contentType: String?) = "https://nostrimg.com/api/upload"
|
||||||
|
|
||||||
override fun parseUrlFromSucess(body: String): String? {
|
override fun parseUrlFromSuccess(body: String): String? {
|
||||||
val tree = jacksonObjectMapper().readTree(body)
|
val tree = jacksonObjectMapper().readTree(body)
|
||||||
val url = tree?.get("data")?.get("link")?.asText()
|
val url = tree?.get("data")?.get("link")?.asText()
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun inputParameterName(contentType: String?): String {
|
||||||
|
return contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
||||||
|
}
|
||||||
|
|
||||||
override fun clientID() = null
|
override fun clientID() = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,21 +159,43 @@ class ImgurServer : FileServer() {
|
|||||||
return if (category == "image") "https://api.imgur.com/3/image" else "https://api.imgur.com/3/upload"
|
return if (category == "image") "https://api.imgur.com/3/image" else "https://api.imgur.com/3/upload"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseUrlFromSucess(body: String): String? {
|
override fun parseUrlFromSuccess(body: String): String? {
|
||||||
val tree = jacksonObjectMapper().readTree(body)
|
val tree = jacksonObjectMapper().readTree(body)
|
||||||
val url = tree?.get("data")?.get("link")?.asText()
|
val url = tree?.get("data")?.get("link")?.asText()
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun inputParameterName(contentType: String?): String {
|
||||||
|
return contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
||||||
|
}
|
||||||
|
|
||||||
override fun clientID() = "Client-ID e6aea87296f3f96"
|
override fun clientID() = "Client-ID e6aea87296f3f96"
|
||||||
}
|
}
|
||||||
|
|
||||||
class NostrBuildServer : FileServer() {
|
class NostrBuildServer : FileServer() {
|
||||||
override fun postUrl(contentType: String?) = "https://nostr.build/api/upload/android.php"
|
override fun postUrl(contentType: String?) = "https://nostr.build/api/upload/android.php"
|
||||||
override fun parseUrlFromSucess(body: String): String? {
|
override fun parseUrlFromSuccess(body: String): String? {
|
||||||
val url = jacksonObjectMapper().readTree(body) // return url.toString()
|
val url = jacksonObjectMapper().readTree(body) // return url.toString()
|
||||||
return url.toString().replace("\"", "")
|
return url.toString().replace("\"", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun inputParameterName(contentType: String?): String {
|
||||||
|
return "fileToUpload"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clientID() = null
|
||||||
|
}
|
||||||
|
|
||||||
|
class NostrFilesDevServer : FileServer() {
|
||||||
|
override fun postUrl(contentType: String?) = "https://nostrfiles.dev/upload_image"
|
||||||
|
override fun parseUrlFromSuccess(body: String): String? {
|
||||||
|
val tree = jacksonObjectMapper().readTree(body)
|
||||||
|
return tree?.get("url")?.asText()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun inputParameterName(contentType: String?): String {
|
||||||
|
return "file"
|
||||||
|
}
|
||||||
|
|
||||||
override fun clientID() = null
|
override fun clientID() = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.vitorpamplona.amethyst.model.*
|
import com.vitorpamplona.amethyst.model.*
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
import kotlinx.coroutines.Dispatchers
|
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
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
open class NewMediaModel : ViewModel() {
|
open class NewMediaModel : ViewModel() {
|
||||||
var account: Account? = null
|
var account: Account? = null
|
||||||
@@ -46,6 +44,8 @@ open class NewMediaModel : ViewModel() {
|
|||||||
selectedServer = ServersAvailable.NOSTRIMG_NIP_94
|
selectedServer = ServersAvailable.NOSTRIMG_NIP_94
|
||||||
} else if (selectedServer == ServersAvailable.NOSTR_BUILD) {
|
} else if (selectedServer == ServersAvailable.NOSTR_BUILD) {
|
||||||
selectedServer = ServersAvailable.NOSTR_BUILD_NIP_94
|
selectedServer = ServersAvailable.NOSTR_BUILD_NIP_94
|
||||||
|
} else if (selectedServer == ServersAvailable.NOSTRFILES_DEV) {
|
||||||
|
selectedServer = ServersAvailable.NOSTRFILES_DEV_NIP_94
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,6 @@ open class NewMediaModel : ViewModel() {
|
|||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
server = serverToUse,
|
server = serverToUse,
|
||||||
context = context,
|
|
||||||
contentResolver = contentResolver,
|
contentResolver = contentResolver,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
onSuccess = { imageUrl, mimeType ->
|
||||||
createNIP94Record(imageUrl, mimeType, description)
|
createNIP94Record(imageUrl, mimeType, description)
|
||||||
@@ -115,18 +114,12 @@ open class NewMediaModel : ViewModel() {
|
|||||||
uploadingDescription.value = "Server Processing"
|
uploadingDescription.value = "Server Processing"
|
||||||
// Images don't seem to be ready immediately after upload
|
// Images don't seem to be ready immediately after upload
|
||||||
|
|
||||||
if (mimeType?.startsWith("image/") == true) {
|
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(imageUrl)
|
||||||
delay(2000)
|
|
||||||
} else {
|
|
||||||
delay(15000)
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadingDescription.value = "Downloading"
|
uploadingDescription.value = "Downloading"
|
||||||
uploadingPercentage.value = 0.60f
|
uploadingPercentage.value = 0.60f
|
||||||
|
|
||||||
try {
|
if (imageData != null) {
|
||||||
val imageData = URL(imageUrl).readBytes()
|
|
||||||
|
|
||||||
uploadingPercentage.value = 0.80f
|
uploadingPercentage.value = 0.80f
|
||||||
uploadingDescription.value = "Hashing"
|
uploadingDescription.value = "Hashing"
|
||||||
|
|
||||||
@@ -154,8 +147,8 @@ open class NewMediaModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} else {
|
||||||
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
|
Log.e("ImageDownload", "Couldn't download image from server")
|
||||||
cancel()
|
cancel()
|
||||||
uploadingPercentage.value = 0.00f
|
uploadingPercentage.value = 0.00f
|
||||||
uploadingDescription.value = null
|
uploadingDescription.value = null
|
||||||
|
|||||||
@@ -109,6 +109,13 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isNIP94Server(selectedServer: ServersAvailable?): Boolean {
|
||||||
|
return selectedServer == ServersAvailable.NOSTRIMG_NIP_94 ||
|
||||||
|
selectedServer == ServersAvailable.IMGUR_NIP_94 ||
|
||||||
|
selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 ||
|
||||||
|
selectedServer == ServersAvailable.NOSTRFILES_DEV_NIP_94
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
|
fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -117,6 +124,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
|
|||||||
Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)),
|
Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)),
|
||||||
Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)),
|
Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)),
|
||||||
Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)),
|
Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)),
|
||||||
|
Triple(ServersAvailable.NOSTRFILES_DEV_NIP_94, stringResource(id = R.string.upload_server_nostrfilesdev_nip94), stringResource(id = R.string.upload_server_nostrfilesdev_nip94_explainer)),
|
||||||
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -191,9 +199,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postViewModel.selectedServer == ServersAvailable.NOSTRIMG_NIP_94 ||
|
if (isNIP94Server(postViewModel.selectedServer) ||
|
||||||
postViewModel.selectedServer == ServersAvailable.IMGUR_NIP_94 ||
|
|
||||||
postViewModel.selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 ||
|
|
||||||
postViewModel.selectedServer == ServersAvailable.NIP95
|
postViewModel.selectedServer == ServersAvailable.NIP95
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -588,9 +588,11 @@ enum class ServersAvailable {
|
|||||||
IMGUR,
|
IMGUR,
|
||||||
NOSTR_BUILD,
|
NOSTR_BUILD,
|
||||||
NOSTRIMG,
|
NOSTRIMG,
|
||||||
|
NOSTRFILES_DEV,
|
||||||
IMGUR_NIP_94,
|
IMGUR_NIP_94,
|
||||||
NOSTRIMG_NIP_94,
|
NOSTRIMG_NIP_94,
|
||||||
NOSTR_BUILD_NIP_94,
|
NOSTR_BUILD_NIP_94,
|
||||||
|
NOSTRFILES_DEV_NIP_94,
|
||||||
NIP95
|
NIP95
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,9 +614,11 @@ fun ImageVideoDescription(
|
|||||||
Triple(ServersAvailable.IMGUR, stringResource(id = R.string.upload_server_imgur), stringResource(id = R.string.upload_server_imgur_explainer)),
|
Triple(ServersAvailable.IMGUR, stringResource(id = R.string.upload_server_imgur), stringResource(id = R.string.upload_server_imgur_explainer)),
|
||||||
Triple(ServersAvailable.NOSTRIMG, stringResource(id = R.string.upload_server_nostrimg), stringResource(id = R.string.upload_server_nostrimg_explainer)),
|
Triple(ServersAvailable.NOSTRIMG, stringResource(id = R.string.upload_server_nostrimg), stringResource(id = R.string.upload_server_nostrimg_explainer)),
|
||||||
Triple(ServersAvailable.NOSTR_BUILD, stringResource(id = R.string.upload_server_nostrbuild), stringResource(id = R.string.upload_server_nostrbuild_explainer)),
|
Triple(ServersAvailable.NOSTR_BUILD, stringResource(id = R.string.upload_server_nostrbuild), stringResource(id = R.string.upload_server_nostrbuild_explainer)),
|
||||||
|
Triple(ServersAvailable.NOSTRFILES_DEV, stringResource(id = R.string.upload_server_nostrfilesdev), stringResource(id = R.string.upload_server_nostrfilesdev_explainer)),
|
||||||
Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)),
|
Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)),
|
||||||
Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)),
|
Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)),
|
||||||
Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)),
|
Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)),
|
||||||
|
Triple(ServersAvailable.NOSTRFILES_DEV_NIP_94, stringResource(id = R.string.upload_server_nostrfilesdev_nip94), stringResource(id = R.string.upload_server_nostrfilesdev_nip94_explainer)),
|
||||||
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -742,9 +746,7 @@ fun ImageVideoDescription(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedServer == ServersAvailable.NOSTRIMG_NIP_94 ||
|
if (isNIP94Server(selectedServer) ||
|
||||||
selectedServer == ServersAvailable.IMGUR_NIP_94 ||
|
|
||||||
selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 ||
|
|
||||||
selectedServer == ServersAvailable.NIP95
|
selectedServer == ServersAvailable.NIP95
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
|||||||
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.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -145,10 +144,9 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
uri = it,
|
uri = it,
|
||||||
server = server,
|
server = server,
|
||||||
context = context,
|
|
||||||
contentResolver = contentResolver,
|
contentResolver = contentResolver,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
onSuccess = { imageUrl, mimeType ->
|
||||||
if (server == ServersAvailable.IMGUR_NIP_94 || server == ServersAvailable.NOSTRIMG_NIP_94 || server == ServersAvailable.NOSTR_BUILD_NIP_94) {
|
if (isNIP94Server(server)) {
|
||||||
createNIP94Record(imageUrl, mimeType, description)
|
createNIP94Record(imageUrl, mimeType, description)
|
||||||
} else {
|
} else {
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
@@ -288,13 +286,6 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String) {
|
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
// Images don't seem to be ready immediately after upload
|
// Images don't seem to be ready immediately after upload
|
||||||
|
|
||||||
if (mimeType?.startsWith("image/") == true) {
|
|
||||||
delay(2000)
|
|
||||||
} else {
|
|
||||||
delay(5000)
|
|
||||||
}
|
|
||||||
|
|
||||||
FileHeader.prepare(
|
FileHeader.prepare(
|
||||||
imageUrl,
|
imageUrl,
|
||||||
mimeType,
|
mimeType,
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
uri = it,
|
uri = it,
|
||||||
server = account.defaultFileServer,
|
server = account.defaultFileServer,
|
||||||
context = context,
|
|
||||||
contentResolver = context.contentResolver,
|
contentResolver = context.contentResolver,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
onSuccess = { imageUrl, mimeType ->
|
||||||
onUploading(false)
|
onUploading(false)
|
||||||
|
|||||||
@@ -5,15 +5,23 @@ 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.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.withStyle
|
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.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
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.model.PrivateDmEvent
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableRoute(
|
fun ClickableRoute(
|
||||||
@@ -21,56 +29,190 @@ fun ClickableRoute(
|
|||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
if (nip19.type == Nip19.Type.USER) {
|
if (nip19.type == Nip19.Type.USER) {
|
||||||
val userBase = LocalCache.getOrCreateUser(nip19.hex)
|
DisplayUser(nip19, navController)
|
||||||
|
|
||||||
val userState by userBase.live().metadata.observeAsState()
|
|
||||||
val user = userState?.user ?: return
|
|
||||||
|
|
||||||
CreateClickableText(user.toBestDisplayName(), nip19.additionalChars, "User/${nip19.hex}", navController)
|
|
||||||
} else if (nip19.type == Nip19.Type.ADDRESS) {
|
} else if (nip19.type == Nip19.Type.ADDRESS) {
|
||||||
val noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
|
DisplayAddress(nip19, navController)
|
||||||
|
} else if (nip19.type == Nip19.Type.NOTE) {
|
||||||
|
DisplayNote(nip19, navController)
|
||||||
|
} else if (nip19.type == Nip19.Type.EVENT) {
|
||||||
|
DisplayEvent(nip19, navController)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
"@${nip19.hex}${nip19.additionalChars} "
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DisplayEvent(
|
||||||
|
nip19: Nip19.Return,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
var noteBase by remember { mutableStateOf<Note?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = nip19.hex) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
noteBase = LocalCache.checkGetOrCreateNote(nip19.hex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noteBase?.let {
|
||||||
|
val noteState by it.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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (noteBase == null) {
|
if (noteBase == null) {
|
||||||
Text(
|
Text(
|
||||||
"@${nip19.hex}${nip19.additionalChars} "
|
"@${nip19.hex}${nip19.additionalChars} "
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
val noteState by noteBase.live().metadata.observeAsState()
|
|
||||||
val note = noteState?.note ?: return
|
|
||||||
|
|
||||||
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Note/${nip19.hex}", navController)
|
|
||||||
}
|
}
|
||||||
} else if (nip19.type == Nip19.Type.NOTE) {
|
}
|
||||||
val noteBase = LocalCache.getOrCreateNote(nip19.hex)
|
|
||||||
val noteState by noteBase.live().metadata.observeAsState()
|
@Composable
|
||||||
|
private fun DisplayNote(
|
||||||
|
nip19: Nip19.Return,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
var noteBase by remember { mutableStateOf<Note?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = nip19.hex) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
noteBase = LocalCache.checkGetOrCreateNote(nip19.hex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noteBase?.let {
|
||||||
|
val noteState by it.live().metadata.observeAsState()
|
||||||
val note = noteState?.note ?: return
|
val note = noteState?.note ?: return
|
||||||
val channel = note.channel()
|
val channel = note.channel()
|
||||||
|
|
||||||
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) {
|
} else if (note.event is PrivateDmEvent) {
|
||||||
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Room/${note.author?.pubkeyHex}", navController)
|
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) {
|
if (noteBase == null) {
|
||||||
CreateClickableText(note.idDisplayNote(), nip19.additionalChars, "Channel/${nip19.hex}", navController)
|
Text(
|
||||||
} else if (note.event is PrivateDmEvent) {
|
"@${nip19.hex}${nip19.additionalChars} "
|
||||||
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 {
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DisplayAddress(
|
||||||
|
nip19: Nip19.Return,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
var noteBase by remember { mutableStateOf<Note?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = nip19.hex) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noteBase?.let {
|
||||||
|
val noteState by it.live().metadata.observeAsState()
|
||||||
|
val note = noteState?.note ?: return
|
||||||
|
|
||||||
|
CreateClickableText(
|
||||||
|
note.idDisplayNote(),
|
||||||
|
nip19.additionalChars,
|
||||||
|
"Note/${nip19.hex}",
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noteBase == null) {
|
||||||
|
Text(
|
||||||
|
"@${nip19.hex}${nip19.additionalChars} "
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DisplayUser(
|
||||||
|
nip19: Nip19.Return,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
var userBase by remember { mutableStateOf<User?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = nip19.hex) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
userBase = LocalCache.checkGetOrCreateUser(nip19.hex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userBase?.let {
|
||||||
|
val userState by it.live().metadata.observeAsState()
|
||||||
|
val user = userState?.user ?: return
|
||||||
|
|
||||||
|
CreateClickableText(
|
||||||
|
user.toBestDisplayName(),
|
||||||
|
nip19.additionalChars,
|
||||||
|
"User/${nip19.hex}",
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userBase == null) {
|
||||||
Text(
|
Text(
|
||||||
"@${nip19.hex}${nip19.additionalChars} "
|
"@${nip19.hex}${nip19.additionalChars} "
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-1
@@ -42,11 +42,13 @@ fun ExpandableRichTextViewer(
|
|||||||
) {
|
) {
|
||||||
var showFullText by remember { mutableStateOf(false) }
|
var showFullText by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val whereToCut = remember(content) {
|
||||||
// Cuts the text in the first space after 350
|
// Cuts the text in the first space after 350
|
||||||
val firstSpaceAfterCut = content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
val firstSpaceAfterCut = content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
||||||
val firstNewLineAfterCut = content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
val firstNewLineAfterCut = content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
||||||
|
|
||||||
val whereToCut = minOf(firstSpaceAfterCut, firstNewLineAfterCut)
|
minOf(firstSpaceAfterCut, firstNewLineAfterCut)
|
||||||
|
}
|
||||||
|
|
||||||
val text = if (showFullText) {
|
val text = if (showFullText) {
|
||||||
content
|
content
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ fun isValidURL(url: String?): Boolean {
|
|||||||
|
|
||||||
val richTextDefaults = RichTextStyle().resolveDefaults()
|
val richTextDefaults = RichTextStyle().resolveDefaults()
|
||||||
|
|
||||||
|
fun isMarkdown(content: String): Boolean {
|
||||||
|
return content.startsWith("> ") ||
|
||||||
|
content.startsWith("# ") ||
|
||||||
|
content.contains("##") ||
|
||||||
|
content.contains("__") ||
|
||||||
|
content.contains("```")
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RichTextViewer(
|
fun RichTextViewer(
|
||||||
content: String,
|
content: String,
|
||||||
@@ -85,58 +93,39 @@ fun RichTextViewer(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
|
val isMarkdown = remember { isMarkdown(content) }
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
if (content.startsWith("> ") ||
|
if (isMarkdown) {
|
||||||
content.startsWith("# ") ||
|
RenderContentAsMarkdown(content, backgroundColor)
|
||||||
content.contains("##") ||
|
|
||||||
content.contains("__") ||
|
|
||||||
content.contains("```")
|
|
||||||
) {
|
|
||||||
val myMarkDownStyle = richTextDefaults.copy(
|
|
||||||
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
|
|
||||||
textStyle = TextStyle(
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 14.sp
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(0.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(shape = RoundedCornerShape(15.dp))
|
|
||||||
.border(
|
|
||||||
1.dp,
|
|
||||||
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
|
||||||
RoundedCornerShape(15.dp)
|
|
||||||
)
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colors.onSurface
|
|
||||||
.copy(alpha = 0.05f)
|
|
||||||
.compositeOver(backgroundColor)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
stringStyle = richTextDefaults.stringStyle?.copy(
|
|
||||||
linkStyle = SpanStyle(
|
|
||||||
textDecoration = TextDecoration.Underline,
|
|
||||||
color = MaterialTheme.colors.primary
|
|
||||||
),
|
|
||||||
codeStyle = SpanStyle(
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val markdownWithSpecialContent = returnMarkdownWithSpecialContent(content)
|
|
||||||
|
|
||||||
MaterialRichText(
|
|
||||||
style = myMarkDownStyle
|
|
||||||
) {
|
|
||||||
Markdown(
|
|
||||||
content = markdownWithSpecialContent,
|
|
||||||
markdownParseOptions = MarkdownParseOptions.Default
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
RenderRegular(content, tags, canPreview, backgroundColor, accountViewModel, navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RichTextViewerState(
|
||||||
|
val content: String,
|
||||||
|
val urlSet: LinkedHashSet<String>,
|
||||||
|
val imagesForPager: Map<String, ZoomableUrlContent>,
|
||||||
|
val imageList: List<ZoomableUrlContent>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderRegular(
|
||||||
|
content: String,
|
||||||
|
tags: List<List<String>>?,
|
||||||
|
canPreview: Boolean,
|
||||||
|
backgroundColor: Color,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
var processedState by remember {
|
||||||
|
mutableStateOf<RichTextViewerState?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = content) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
|
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
|
||||||
val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl }
|
val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl }
|
||||||
val imagesForPager = urlSet.mapNotNull { fullUrl ->
|
val imagesForPager = urlSet.mapNotNull { fullUrl ->
|
||||||
@@ -151,17 +140,27 @@ fun RichTextViewer(
|
|||||||
}.associateBy { it.url }
|
}.associateBy { it.url }
|
||||||
val imageList = imagesForPager.values.toList()
|
val imageList = imagesForPager.values.toList()
|
||||||
|
|
||||||
|
processedState = RichTextViewerState(content, urlSet, imagesForPager, imageList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
processedState?.let { state ->
|
||||||
content.split('\n').forEach { paragraph ->
|
content.split('\n').forEach { paragraph ->
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
val s = if (isArabic(paragraph)) paragraph.trim().split(' ').reversed() else paragraph.trim().split(' ')
|
val s = if (isArabic(paragraph)) {
|
||||||
|
paragraph.trim().split(' ')
|
||||||
|
.reversed()
|
||||||
|
} else {
|
||||||
|
paragraph.trim().split(' ')
|
||||||
|
}
|
||||||
s.forEach { word: String ->
|
s.forEach { word: String ->
|
||||||
if (canPreview) {
|
if (canPreview) {
|
||||||
// Explicit URL
|
// Explicit URL
|
||||||
val img = imagesForPager[word]
|
val img = state.imagesForPager[word]
|
||||||
if (img != null) {
|
if (img != null) {
|
||||||
ZoomableContentView(img, imageList)
|
ZoomableContentView(img, state.imageList)
|
||||||
} else if (urlSet.contains(word)) {
|
} else if (state.urlSet.contains(word)) {
|
||||||
UrlPreview(word, "$word ")
|
UrlPreview(word, "$word ")
|
||||||
} else if (word.startsWith("lnbc", true)) {
|
} else if (word.startsWith("lnbc", true)) {
|
||||||
MayBeInvoicePreview(word)
|
MayBeInvoicePreview(word)
|
||||||
@@ -219,7 +218,7 @@ fun RichTextViewer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (urlSet.contains(word)) {
|
if (state.urlSet.contains(word)) {
|
||||||
ClickableUrl("$word ", word)
|
ClickableUrl("$word ", word)
|
||||||
} else if (word.startsWith("lnurl", true)) {
|
} else if (word.startsWith("lnurl", true)) {
|
||||||
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
|
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
|
||||||
@@ -287,25 +286,140 @@ fun RichTextViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getDisplayNameFromUserNip19(parsedNip19: Nip19.Return): String? {
|
private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) {
|
||||||
if (parsedNip19.type == Nip19.Type.USER) {
|
val myMarkDownStyle = richTextDefaults.copy(
|
||||||
val userHex = parsedNip19.hex
|
codeBlockStyle = richTextDefaults.codeBlockStyle?.copy(
|
||||||
val userBase = LocalCache.getOrCreateUser(userHex)
|
textStyle = TextStyle(
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 14.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(0.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(shape = RoundedCornerShape(15.dp))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||||
|
RoundedCornerShape(15.dp)
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colors.onSurface
|
||||||
|
.copy(alpha = 0.05f)
|
||||||
|
.compositeOver(backgroundColor)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
stringStyle = richTextDefaults.stringStyle?.copy(
|
||||||
|
linkStyle = SpanStyle(
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
color = MaterialTheme.colors.primary
|
||||||
|
),
|
||||||
|
codeStyle = SpanStyle(
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f)
|
||||||
|
.compositeOver(backgroundColor)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val userState by userBase.live().metadata.observeAsState()
|
var markdownWithSpecialContent by remember { mutableStateOf<String?>(null) }
|
||||||
val displayName = userState?.user?.bestDisplayName()
|
var nip19References by remember { mutableStateOf<List<Nip19.Return>>(emptyList()) }
|
||||||
if (displayName !== null) {
|
var refresh by remember { mutableStateOf(0) }
|
||||||
return displayName
|
|
||||||
|
LaunchedEffect(key1 = content) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
nip19References = returnNIP19References(content)
|
||||||
|
markdownWithSpecialContent = returnMarkdownWithSpecialContent(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = refresh) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content)
|
||||||
|
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
|
||||||
|
markdownWithSpecialContent = newMarkdownWithSpecialContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nip19References.forEach {
|
||||||
|
var baseUser by remember { mutableStateOf<User?>(null) }
|
||||||
|
var baseNote by remember { mutableStateOf<Note?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = it.hex) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
|
||||||
|
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
|
||||||
|
baseNote = note
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.type == Nip19.Type.USER) {
|
||||||
|
LocalCache.checkGetOrCreateUser(it.hex)?.let { user ->
|
||||||
|
baseUser = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseNote?.let {
|
||||||
|
val noteState by it.live().metadata.observeAsState()
|
||||||
|
if (noteState?.note?.event != null) {
|
||||||
|
refresh++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
baseUser?.let {
|
||||||
|
val userState by it.live().metadata.observeAsState()
|
||||||
|
if (userState?.user?.info != null) {
|
||||||
|
refresh++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markdownWithSpecialContent?.let {
|
||||||
|
MaterialRichText(
|
||||||
|
style = myMarkDownStyle
|
||||||
|
) {
|
||||||
|
Markdown(
|
||||||
|
content = it,
|
||||||
|
markdownParseOptions = MarkdownParseOptions.Default
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDisplayNameFromNip19(nip19: Nip19.Return): String? {
|
||||||
|
if (nip19.type == Nip19.Type.USER) {
|
||||||
|
return LocalCache.users[nip19.hex]?.bestDisplayName()
|
||||||
|
} else if (nip19.type == Nip19.Type.NOTE) {
|
||||||
|
return LocalCache.notes[nip19.hex]?.idDisplayNote()
|
||||||
|
} else if (nip19.type == Nip19.Type.ADDRESS) {
|
||||||
|
return LocalCache.addressables[nip19.hex]?.idDisplayNote()
|
||||||
|
} else if (nip19.type == Nip19.Type.EVENT) {
|
||||||
|
return LocalCache.notes[nip19.hex]?.idDisplayNote() ?: LocalCache.addressables[nip19.hex]?.idDisplayNote()
|
||||||
|
} else {
|
||||||
return null
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun returnNIP19References(content: String): List<Nip19.Return> {
|
||||||
|
val listOfReferences = mutableListOf<Nip19.Return>()
|
||||||
|
content.split('\n').forEach { paragraph ->
|
||||||
|
paragraph.split(' ').forEach { word: String ->
|
||||||
|
if (isBechLink(word)) {
|
||||||
|
val parsedNip19 = Nip19.uriToRoute(word)
|
||||||
|
parsedNip19?.let {
|
||||||
|
listOfReferences.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listOfReferences
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun returnMarkdownWithSpecialContent(content: String): String {
|
private fun returnMarkdownWithSpecialContent(content: String): String {
|
||||||
var returnContent = ""
|
var returnContent = ""
|
||||||
content.split('\n').forEach { paragraph ->
|
content.split('\n').forEach { paragraph ->
|
||||||
@@ -319,7 +433,7 @@ private fun returnMarkdownWithSpecialContent(content: String): String {
|
|||||||
} else if (isBechLink(word)) {
|
} else if (isBechLink(word)) {
|
||||||
val parsedNip19 = Nip19.uriToRoute(word)
|
val parsedNip19 = Nip19.uriToRoute(word)
|
||||||
returnContent += if (parsedNip19 !== null) {
|
returnContent += if (parsedNip19 !== null) {
|
||||||
val displayName = getDisplayNameFromUserNip19(parsedNip19)
|
val displayName = getDisplayNameFromNip19(parsedNip19)
|
||||||
if (displayName != null) {
|
if (displayName != null) {
|
||||||
"[@$displayName](nostr://$word) "
|
"[@$displayName](nostr://$word) "
|
||||||
} else {
|
} else {
|
||||||
@@ -359,9 +473,9 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountV
|
|||||||
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
|
LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
|
||||||
baseNotePair = Pair(note, it.additionalChars)
|
baseNotePair = Pair(note, it.additionalChars)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
nip19Route = it
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nip19Route = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,8 +494,7 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountV
|
|||||||
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||||
RoundedCornerShape(15.dp)
|
RoundedCornerShape(15.dp)
|
||||||
),
|
),
|
||||||
parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)
|
parentBackgroundColor = backgroundColor,
|
||||||
.compositeOver(backgroundColor),
|
|
||||||
isQuotedNote = true,
|
isQuotedNote = true,
|
||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
@@ -530,8 +643,7 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
|
|||||||
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||||
RoundedCornerShape(15.dp)
|
RoundedCornerShape(15.dp)
|
||||||
),
|
),
|
||||||
parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f)
|
parentBackgroundColor = backgroundColor,
|
||||||
.compositeOver(backgroundColor),
|
|
||||||
isQuotedNote = true,
|
isQuotedNote = true,
|
||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ object Robohash {
|
|||||||
.data("robohash:$message")
|
.data("robohash:$message")
|
||||||
.fetcherFactory(HashImageFetcher.Factory)
|
.fetcherFactory(HashImageFetcher.Factory)
|
||||||
.size(robotSize)
|
.size(robotSize)
|
||||||
.crossfade(100)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.baha.url.preview.IUrlPreviewCallback
|
import com.baha.url.preview.IUrlPreviewCallback
|
||||||
@@ -14,7 +15,7 @@ import com.baha.url.preview.UrlInfoItem
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UrlPreview(url: String, urlText: String) {
|
fun UrlPreview(url: String, urlText: String) {
|
||||||
@@ -28,11 +29,12 @@ fun UrlPreview(url: String, urlText: String) {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
var urlPreviewState by remember { mutableStateOf<UrlPreviewState>(default) }
|
var urlPreviewState by remember { mutableStateOf<UrlPreviewState>(default) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
|
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
|
||||||
LaunchedEffect(url) {
|
LaunchedEffect(url) {
|
||||||
if (urlPreviewState == UrlPreviewState.Loading) {
|
if (urlPreviewState == UrlPreviewState.Loading) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
UrlCachedPreviewer.previewInfo(
|
UrlCachedPreviewer.previewInfo(
|
||||||
url,
|
url,
|
||||||
object : IUrlPreviewCallback {
|
object : IUrlPreviewCallback {
|
||||||
|
|||||||
@@ -60,8 +60,10 @@ import java.io.File
|
|||||||
public var muted = mutableStateOf(true)
|
public var muted = mutableStateOf(true)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoView(localFile: File, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) {
|
fun VideoView(localFile: File?, description: String? = null, onDialog: ((Boolean) -> Unit)? = null) {
|
||||||
|
if (localFile != null) {
|
||||||
VideoView(localFile.toUri(), description, onDialog)
|
VideoView(localFile.toUri(), description, onDialog)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ import com.vitorpamplona.amethyst.ui.theme.Nip05
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.engawapg.lib.zoomable.rememberZoomState
|
import net.engawapg.lib.zoomable.rememberZoomState
|
||||||
import net.engawapg.lib.zoomable.zoomable
|
import net.engawapg.lib.zoomable.zoomable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -112,7 +111,7 @@ class ZoomableUrlVideo(
|
|||||||
) : ZoomableUrlContent(url, description, hash, dim, uri)
|
) : ZoomableUrlContent(url, description, hash, dim, uri)
|
||||||
|
|
||||||
abstract class ZoomablePreloadedContent(
|
abstract class ZoomablePreloadedContent(
|
||||||
val localFile: File,
|
val localFile: File?,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
val mimeType: String? = null,
|
val mimeType: String? = null,
|
||||||
val isVerified: Boolean? = null,
|
val isVerified: Boolean? = null,
|
||||||
@@ -121,7 +120,7 @@ abstract class ZoomablePreloadedContent(
|
|||||||
) : ZoomableContent(description, dim)
|
) : ZoomableContent(description, dim)
|
||||||
|
|
||||||
class ZoomableLocalImage(
|
class ZoomableLocalImage(
|
||||||
localFile: File,
|
localFile: File?,
|
||||||
mimeType: String? = null,
|
mimeType: String? = null,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
val blurhash: String? = null,
|
val blurhash: String? = null,
|
||||||
@@ -131,7 +130,7 @@ class ZoomableLocalImage(
|
|||||||
) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri)
|
) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri)
|
||||||
|
|
||||||
class ZoomableLocalVideo(
|
class ZoomableLocalVideo(
|
||||||
localFile: File,
|
localFile: File?,
|
||||||
mimeType: String? = null,
|
mimeType: String? = null,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
dim: String? = null,
|
dim: String? = null,
|
||||||
@@ -210,7 +209,9 @@ private fun LocalImageView(
|
|||||||
mutableStateOf<AsyncImagePainter.State?>(null)
|
mutableStateOf<AsyncImagePainter.State?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ratio = aspectRatio(content.dim)
|
val ratio = remember {
|
||||||
|
aspectRatio(content.dim)
|
||||||
|
}
|
||||||
|
|
||||||
BoxWithConstraints(contentAlignment = Alignment.Center) {
|
BoxWithConstraints(contentAlignment = Alignment.Center) {
|
||||||
val myModifier = mainImageModifier.also {
|
val myModifier = mainImageModifier.also {
|
||||||
@@ -220,7 +221,7 @@ private fun LocalImageView(
|
|||||||
}
|
}
|
||||||
val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
|
val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
|
||||||
|
|
||||||
if (content.localFile.exists()) {
|
if (content.localFile != null && content.localFile.exists()) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = content.localFile,
|
model = content.localFile,
|
||||||
contentDescription = content.description,
|
contentDescription = content.description,
|
||||||
@@ -247,7 +248,7 @@ private fun LocalImageView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageState is AsyncImagePainter.State.Error || !content.localFile.exists()) {
|
if (imageState is AsyncImagePainter.State.Error || content.localFile == null || !content.localFile.exists()) {
|
||||||
BlankNote()
|
BlankNote()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,7 +271,9 @@ private fun UrlImageView(
|
|||||||
mutableStateOf<Boolean?>(null)
|
mutableStateOf<Boolean?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val ratio = aspectRatio(content.dim)
|
val ratio = remember {
|
||||||
|
aspectRatio(content.dim)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = content.url, key2 = imageState) {
|
LaunchedEffect(key1 = content.url, key2 = imageState) {
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
if (imageState is AsyncImagePainter.State.Success) {
|
||||||
@@ -337,9 +340,10 @@ private fun aspectRatio(dim: String?): Float? {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DisplayUrlWithLoadingSymbol(content: ZoomableContent) {
|
private fun DisplayUrlWithLoadingSymbol(content: ZoomableContent) {
|
||||||
var cnt by remember { mutableStateOf<ZoomableContent?>(null) }
|
var cnt by remember { mutableStateOf<ZoomableContent?>(null) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
delay(200)
|
delay(200)
|
||||||
cnt = content
|
cnt = content
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.dal
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Channel
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
|
||||||
object ChannelFeedFilter : AdditiveFeedFilter<Note>() {
|
object ChannelFeedFilter : AdditiveFeedFilter<Note>() {
|
||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
lateinit var channel: Channel
|
var channelId: String? = null
|
||||||
|
|
||||||
fun loadMessagesBetween(accountLoggedIn: Account, channelId: String) {
|
fun loadMessagesBetween(accountLoggedIn: Account, channelId: String?) {
|
||||||
account = accountLoggedIn
|
this.account = accountLoggedIn
|
||||||
channel = LocalCache.getOrCreateChannel(channelId)
|
this.channelId = channelId
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
|
val processingChannel = channelId ?: return emptyList()
|
||||||
|
val channel = LocalCache.getOrCreateChannel(processingChannel)
|
||||||
|
|
||||||
return channel.notes
|
return channel.notes
|
||||||
.values
|
.values
|
||||||
.filter { account.isAcceptable(it) }
|
.filter { account.isAcceptable(it) }
|
||||||
@@ -24,6 +26,9 @@ object ChannelFeedFilter : AdditiveFeedFilter<Note>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||||
|
val processingChannel = channelId ?: return emptySet()
|
||||||
|
val channel = LocalCache.getOrCreateChannel(processingChannel)
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
.filter { it.idHex in channel.notes.keys && account.isAcceptable(it) }
|
.filter { it.idHex in channel.notes.keys && account.isAcceptable(it) }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|||||||
@@ -3,21 +3,22 @@ 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.model.User
|
|
||||||
|
|
||||||
object ChatroomFeedFilter : AdditiveFeedFilter<Note>() {
|
object ChatroomFeedFilter : AdditiveFeedFilter<Note>() {
|
||||||
var account: Account? = null
|
var account: Account? = null
|
||||||
var withUser: User? = null
|
var withUser: String? = null
|
||||||
|
|
||||||
fun loadMessagesBetween(accountIn: Account, userId: String) {
|
fun loadMessagesBetween(accountIn: Account, userId: String) {
|
||||||
account = accountIn
|
account = accountIn
|
||||||
withUser = LocalCache.checkGetOrCreateUser(userId)
|
withUser = userId
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
|
val processingUser = withUser ?: return emptyList()
|
||||||
|
|
||||||
val myAccount = account
|
val myAccount = account
|
||||||
val myUser = withUser
|
val myUser = LocalCache.checkGetOrCreateUser(processingUser)
|
||||||
|
|
||||||
if (myAccount == null || myUser == null) return emptyList()
|
if (myAccount == null || myUser == null) return emptyList()
|
||||||
|
|
||||||
@@ -32,8 +33,10 @@ object ChatroomFeedFilter : AdditiveFeedFilter<Note>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||||
|
val processingUser = withUser ?: return emptySet()
|
||||||
|
|
||||||
val myAccount = account
|
val myAccount = account
|
||||||
val myUser = withUser
|
val myUser = LocalCache.checkGetOrCreateUser(processingUser)
|
||||||
|
|
||||||
if (myAccount == null || myUser == null) return emptySet()
|
if (myAccount == null || myUser == null) return emptySet()
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -9,9 +9,9 @@ object UserProfileBookmarksFeedFilter : FeedFilter<Note>() {
|
|||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(accountLoggedIn: Account, userId: String) {
|
fun loadUserProfile(accountLoggedIn: Account, user: User?) {
|
||||||
account = accountLoggedIn
|
account = accountLoggedIn
|
||||||
user = LocalCache.users[userId]
|
this.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
|
|||||||
+2
-3
@@ -1,7 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.dal
|
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.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
|
||||||
@@ -9,9 +8,9 @@ object UserProfileConversationsFeedFilter : FeedFilter<Note>() {
|
|||||||
var account: Account? = null
|
var account: Account? = null
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(accountLoggedIn: Account, userId: String) {
|
fun loadUserProfile(accountLoggedIn: Account, user: User?) {
|
||||||
account = accountLoggedIn
|
account = accountLoggedIn
|
||||||
user = LocalCache.checkGetOrCreateUser(userId)
|
this.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
|
|||||||
+2
-2
@@ -8,9 +8,9 @@ object UserProfileFollowersFeedFilter : FeedFilter<User>() {
|
|||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(accountLoggedIn: Account, userId: String) {
|
fun loadUserProfile(accountLoggedIn: Account, user: User?) {
|
||||||
account = accountLoggedIn
|
account = accountLoggedIn
|
||||||
user = LocalCache.users[userId]
|
this.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<User> {
|
override fun feed(): List<User> {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ object UserProfileFollowsFeedFilter : FeedFilter<User>() {
|
|||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(accountLoggedIn: Account, userId: String) {
|
fun loadUserProfile(accountLoggedIn: Account, user: User?) {
|
||||||
account = accountLoggedIn
|
account = accountLoggedIn
|
||||||
user = LocalCache.users[userId]
|
this.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<User> {
|
override fun feed(): List<User> {
|
||||||
|
|||||||
+2
-2
@@ -9,9 +9,9 @@ object UserProfileNewThreadFeedFilter : FeedFilter<Note>() {
|
|||||||
var account: Account? = null
|
var account: Account? = null
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(accountLoggedIn: Account, userId: String) {
|
fun loadUserProfile(accountLoggedIn: Account, user: User) {
|
||||||
account = accountLoggedIn
|
account = accountLoggedIn
|
||||||
user = LocalCache.checkGetOrCreateUser(userId)
|
this.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.dal
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
object UserProfileReportsFeedFilter : FeedFilter<Note>() {
|
object UserProfileReportsFeedFilter : FeedFilter<Note>() {
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(userId: String) {
|
fun loadUserProfile(user: User?) {
|
||||||
user = LocalCache.checkGetOrCreateUser(userId)
|
this.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.dal
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.model.zaps.UserZaps
|
import com.vitorpamplona.amethyst.service.model.zaps.UserZaps
|
||||||
@@ -8,8 +7,8 @@ import com.vitorpamplona.amethyst.service.model.zaps.UserZaps
|
|||||||
object UserProfileZapsFeedFilter : FeedFilter<Pair<Note, Note>>() {
|
object UserProfileZapsFeedFilter : FeedFilter<Pair<Note, Note>>() {
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
|
|
||||||
fun loadUserProfile(userId: String) {
|
fun loadUserProfile(user: User?) {
|
||||||
user = LocalCache.checkGetOrCreateUser(userId)
|
this.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Pair<Note, Note>> {
|
override fun feed(): List<Pair<Note, Note>> {
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
val bottomNavigationItems = listOf(
|
val bottomNavigationItems = listOf(
|
||||||
@@ -159,15 +158,16 @@ private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: Ac
|
|||||||
val notif = notifState.value ?: return
|
val notif = notifState.value ?: return
|
||||||
|
|
||||||
var hasNewItems by remember { mutableStateOf<Boolean>(false) }
|
var hasNewItems by remember { mutableStateOf<Boolean>(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(key1 = notif) {
|
LaunchedEffect(key1 = notif) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
hasNewItems = route.hasNewItems(account, notif.cache, emptySet())
|
hasNewItems = route.hasNewItems(account, notif.cache, emptySet())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = db) {
|
LaunchedEffect(key1 = db) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
hasNewItems = route.hasNewItems(account, notif.cache, db)
|
hasNewItems = route.hasNewItems(account, notif.cache, db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||||
@@ -87,7 +88,7 @@ fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, ac
|
|||||||
@Composable
|
@Composable
|
||||||
fun StoriesTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun StoriesTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
FollowList(account.defaultStoriesFollowList, true) { listName ->
|
FollowList(account.defaultStoriesFollowList, account.userProfile(), true) { listName ->
|
||||||
account.changeDefaultStoriesFollowList(listName)
|
account.changeDefaultStoriesFollowList(listName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,7 @@ fun StoriesTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
|||||||
@Composable
|
@Composable
|
||||||
fun HomeTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun HomeTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
FollowList(account.defaultHomeFollowList, false) { listName ->
|
FollowList(account.defaultHomeFollowList, account.userProfile(), false) { listName ->
|
||||||
account.changeDefaultHomeFollowList(listName)
|
account.changeDefaultHomeFollowList(listName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +227,7 @@ private fun LoggedInUserPictureDrawer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FollowList(listName: String, withGlobal: Boolean, onChange: (String) -> Unit) {
|
fun FollowList(listName: String, loggedIn: User, withGlobal: Boolean, onChange: (String) -> Unit) {
|
||||||
// Notification
|
// Notification
|
||||||
val dbState = LocalCache.live.observeAsState()
|
val dbState = LocalCache.live.observeAsState()
|
||||||
val db = dbState.value ?: return
|
val db = dbState.value ?: return
|
||||||
@@ -244,7 +245,7 @@ fun FollowList(listName: String, withGlobal: Boolean, onChange: (String) -> Unit
|
|||||||
followLists = defaultOptions + LocalCache.addressables.mapNotNull {
|
followLists = defaultOptions + LocalCache.addressables.mapNotNull {
|
||||||
val event = (it.value.event as? PeopleListEvent)
|
val event = (it.value.event as? PeopleListEvent)
|
||||||
// Has to have an list
|
// Has to have an list
|
||||||
if (event != null && (event.tags.size > 1 || event.content.length > 50)) {
|
if (event != null && event.pubKey == loggedIn.pubkeyHex && (event.tags.size > 1 || event.content.length > 50)) {
|
||||||
Pair(event.dTag(), event.dTag())
|
Pair(event.dTag(), event.dTag())
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -254,7 +255,7 @@ fun FollowList(listName: String, withGlobal: Boolean, onChange: (String) -> Unit
|
|||||||
}
|
}
|
||||||
|
|
||||||
SimpleTextSpinner(
|
SimpleTextSpinner(
|
||||||
placeholder = followLists.firstOrNull { it.first == listName }?.first ?: KIND3_FOLLOWS,
|
placeholder = followLists.firstOrNull { it.first == listName }?.second ?: "Select an Option",
|
||||||
options = followNames.value,
|
options = followNames.value,
|
||||||
onSelect = {
|
onSelect = {
|
||||||
onChange(followLists.getOrNull(it)?.first ?: KIND3_FOLLOWS)
|
onChange(followLists.getOrNull(it)?.first ?: KIND3_FOLLOWS)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -34,11 +35,10 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.vitorpamplona.amethyst.NotificationCache
|
import com.vitorpamplona.amethyst.NotificationCache
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
|
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -48,8 +48,8 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
|||||||
|
|
||||||
val context = LocalContext.current.applicationContext
|
val context = LocalContext.current.applicationContext
|
||||||
|
|
||||||
val noteEvent = note?.event
|
|
||||||
var popupExpanded by remember { mutableStateOf(false) }
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
BlankNote(Modifier, isInnerNote)
|
BlankNote(Modifier, isInnerNote)
|
||||||
@@ -57,7 +57,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
|||||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = likeSetCard) {
|
LaunchedEffect(key1 = likeSetCard) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
isNew = likeSetCard.createdAt() > NotificationCache.load(routeForLastRead)
|
isNew = likeSetCard.createdAt() > NotificationCache.load(routeForLastRead)
|
||||||
|
|
||||||
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt())
|
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt())
|
||||||
@@ -71,17 +71,14 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.background(backgroundColor).combinedClickable(
|
modifier = Modifier
|
||||||
|
.background(backgroundColor)
|
||||||
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (noteEvent !is ChannelMessageEvent) {
|
routeFor(
|
||||||
navController.navigate("Note/${note.idHex}") {
|
note,
|
||||||
launchSingleTop = true
|
accountViewModel.userProfile()
|
||||||
}
|
)?.let { navController.navigate(it) }
|
||||||
} else {
|
|
||||||
note.channel()?.let {
|
|
||||||
navController.navigate("Channel/${it.idHex}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
)
|
||||||
@@ -104,7 +101,9 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.MilitaryTech,
|
imageVector = Icons.Default.MilitaryTech,
|
||||||
null,
|
null,
|
||||||
modifier = Modifier.size(25.dp).align(Alignment.TopEnd),
|
modifier = Modifier
|
||||||
|
.size(25.dp)
|
||||||
|
.align(Alignment.TopEnd),
|
||||||
tint = MaterialTheme.colors.primary
|
tint = MaterialTheme.colors.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -115,7 +114,9 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
|||||||
Text(
|
Text(
|
||||||
stringResource(R.string.new_badge_award_notif),
|
stringResource(R.string.new_badge_award_notif),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.padding(bottom = 5.dp).weight(1f)
|
modifier = Modifier
|
||||||
|
.padding(bottom = 5.dp)
|
||||||
|
.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ fun HiddenNote(reports: Set<Note>, loggedIn: User, modifier: Modifier = Modifier
|
|||||||
FlowRow(modifier = Modifier.padding(top = 10.dp)) {
|
FlowRow(modifier = Modifier.padding(top = 10.dp)) {
|
||||||
reports.forEach {
|
reports.forEach {
|
||||||
NoteAuthorPicture(
|
NoteAuthorPicture(
|
||||||
note = it,
|
baseNote = it,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
userAccount = loggedIn,
|
userAccount = loggedIn,
|
||||||
size = 35.dp
|
size = 35.dp
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import androidx.navigation.NavController
|
|||||||
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.service.model.ChannelMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.BoostSetCard
|
import com.vitorpamplona.amethyst.ui.screen.BoostSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -68,15 +67,7 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.background(backgroundColor).combinedClickable(
|
modifier = Modifier.background(backgroundColor).combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (noteEvent !is ChannelMessageEvent) {
|
routeFor(note, account.userProfile())?.let { navController.navigate(it) }
|
||||||
navController.navigate("Note/${note.idHex}") {
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
note.channel()?.let {
|
|
||||||
navController.navigate("Channel/${it.idHex}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
)
|
||||||
@@ -109,7 +100,7 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
|
|||||||
FlowRow() {
|
FlowRow() {
|
||||||
boostSetCard.boostEvents.forEach {
|
boostSetCard.boostEvents.forEach {
|
||||||
NoteAuthorPicture(
|
NoteAuthorPicture(
|
||||||
note = it,
|
baseNote = it,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
userAccount = account.userProfile(),
|
userAccount = account.userProfile(),
|
||||||
size = 35.dp
|
size = 35.dp
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -50,7 +51,7 @@ import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
|||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatroomCompose(
|
fun ChatroomCompose(
|
||||||
@@ -64,6 +65,8 @@ fun ChatroomCompose(
|
|||||||
val notificationCacheState = NotificationCache.live.observeAsState()
|
val notificationCacheState = NotificationCache.live.observeAsState()
|
||||||
val notificationCache = notificationCacheState.value ?: return
|
val notificationCache = notificationCacheState.value ?: return
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (note?.event == null) {
|
if (note?.event == null) {
|
||||||
BlankNote(Modifier)
|
BlankNote(Modifier)
|
||||||
} else if (note.channel() != null) {
|
} else if (note.channel() != null) {
|
||||||
@@ -86,7 +89,7 @@ fun ChatroomCompose(
|
|||||||
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = notificationCache, key2 = note) {
|
LaunchedEffect(key1 = notificationCache, key2 = note) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
note.createdAt()?.let { timestamp ->
|
note.createdAt()?.let { timestamp ->
|
||||||
hasNewMessages =
|
hasNewMessages =
|
||||||
timestamp > notificationCache.cache.load("Channel/${chan.idHex}")
|
timestamp > notificationCache.cache.load("Channel/${chan.idHex}")
|
||||||
@@ -131,7 +134,7 @@ fun ChatroomCompose(
|
|||||||
} else {
|
} else {
|
||||||
val replyAuthorBase =
|
val replyAuthorBase =
|
||||||
(note.event as? PrivateDmEvent)
|
(note.event as? PrivateDmEvent)
|
||||||
?.recipientPubKey()
|
?.verifiedRecipientPubKey()
|
||||||
?.let { LocalCache.getOrCreateUser(it) }
|
?.let { LocalCache.getOrCreateUser(it) }
|
||||||
|
|
||||||
var userToComposeOn = note.author!!
|
var userToComposeOn = note.author!!
|
||||||
@@ -148,7 +151,7 @@ fun ChatroomCompose(
|
|||||||
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = notificationCache, key2 = note) {
|
LaunchedEffect(key1 = notificationCache, key2 = note) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
noteEvent?.let {
|
noteEvent?.let {
|
||||||
hasNewMessages = it.createdAt() > notificationCache.cache.load(
|
hasNewMessages = it.createdAt() > notificationCache.cache.load(
|
||||||
"Room/${userToComposeOn.pubkeyHex}"
|
"Room/${userToComposeOn.pubkeyHex}"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -63,7 +64,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
|||||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
|
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
|
||||||
val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
|
val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
|
||||||
@@ -94,6 +95,7 @@ fun ChatroomMessageCompose(
|
|||||||
var showHiddenNote by remember { mutableStateOf(false) }
|
var showHiddenNote by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val context = LocalContext.current.applicationContext
|
val context = LocalContext.current.applicationContext
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (note?.event == null) {
|
if (note?.event == null) {
|
||||||
BlankNote(Modifier)
|
BlankNote(Modifier)
|
||||||
@@ -135,7 +137,7 @@ fun ChatroomMessageCompose(
|
|||||||
|
|
||||||
LaunchedEffect(key1 = routeForLastRead) {
|
LaunchedEffect(key1 = routeForLastRead) {
|
||||||
routeForLastRead?.let {
|
routeForLastRead?.let {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
val lastTime = NotificationCache.load(it)
|
val lastTime = NotificationCache.load(it)
|
||||||
|
|
||||||
val createdAt = note.createdAt()
|
val createdAt = note.createdAt()
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import androidx.navigation.NavController
|
|||||||
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.service.model.ChannelMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
|
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -68,15 +67,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, route
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.background(backgroundColor).combinedClickable(
|
modifier = Modifier.background(backgroundColor).combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (noteEvent !is ChannelMessageEvent) {
|
routeFor(note, account.userProfile())?.let { navController.navigate(it) }
|
||||||
navController.navigate("Note/${note.idHex}") {
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
note.channel()?.let {
|
|
||||||
navController.navigate("Channel/${it.idHex}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
)
|
||||||
@@ -109,7 +100,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, route
|
|||||||
FlowRow() {
|
FlowRow() {
|
||||||
likeSetCard.likeEvents.forEach {
|
likeSetCard.likeEvents.forEach {
|
||||||
NoteAuthorPicture(
|
NoteAuthorPicture(
|
||||||
note = it,
|
baseNote = it,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
userAccount = account.userProfile(),
|
userAccount = account.userProfile(),
|
||||||
size = 35.dp
|
size = 35.dp
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -26,13 +27,10 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
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.LocalCache
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -40,20 +38,25 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal
|
|||||||
val noteState by messageSetCard.note.live().metadata.observeAsState()
|
val noteState by messageSetCard.note.live().metadata.observeAsState()
|
||||||
val note = noteState?.note
|
val note = noteState?.note
|
||||||
|
|
||||||
val noteEvent = note?.event
|
|
||||||
var popupExpanded by remember { mutableStateOf(false) }
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
BlankNote(Modifier, isInnerNote)
|
BlankNote(Modifier, isInnerNote)
|
||||||
} else {
|
} else {
|
||||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = messageSetCard.createdAt()) {
|
LaunchedEffect(key1 = messageSetCard.createdAt()) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
isNew =
|
val newIsNew =
|
||||||
messageSetCard.createdAt() > NotificationCache.load(routeForLastRead)
|
messageSetCard.createdAt() > NotificationCache.load(routeForLastRead)
|
||||||
|
|
||||||
NotificationCache.markAsRead(routeForLastRead, messageSetCard.createdAt())
|
NotificationCache.markAsRead(routeForLastRead, messageSetCard.createdAt())
|
||||||
|
|
||||||
|
if (newIsNew != isNew) {
|
||||||
|
isNew = newIsNew
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,30 +69,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.background(backgroundColor).combinedClickable(
|
modifier = Modifier.background(backgroundColor).combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (noteEvent is ChannelMessageEvent) {
|
routeFor(note, accountViewModel.userProfile())?.let { navController.navigate(it) }
|
||||||
note.channel()?.let {
|
|
||||||
navController.navigate("Channel/${it.idHex}")
|
|
||||||
}
|
|
||||||
} else if (noteEvent is PrivateDmEvent) {
|
|
||||||
val replyAuthorBase =
|
|
||||||
(note.event as? PrivateDmEvent)
|
|
||||||
?.recipientPubKey()
|
|
||||||
?.let { LocalCache.getOrCreateUser(it) }
|
|
||||||
|
|
||||||
var userToComposeOn = note.author!!
|
|
||||||
|
|
||||||
if (replyAuthorBase != null) {
|
|
||||||
if (note.author == accountViewModel.userProfile()) {
|
|
||||||
userToComposeOn = replyAuthorBase
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navController.navigate("Room/${userToComposeOn.pubkeyHex}")
|
|
||||||
} else {
|
|
||||||
navController.navigate("Note/${note.idHex}") {
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
)
|
||||||
@@ -120,7 +100,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal
|
|||||||
|
|
||||||
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
|
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
|
||||||
NoteCompose(
|
NoteCompose(
|
||||||
baseNote = note,
|
baseNote = messageSetCard.note,
|
||||||
routeForLastRead = null,
|
routeForLastRead = null,
|
||||||
isBoostedNote = true,
|
isBoostedNote = true,
|
||||||
addMarginTop = false,
|
addMarginTop = false,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.note
|
|||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -13,6 +14,7 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
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.Bolt
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -21,6 +23,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -37,15 +40,13 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -56,19 +57,24 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
|||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
val noteEvent = note?.event
|
|
||||||
var popupExpanded by remember { mutableStateOf(false) }
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
BlankNote(Modifier, false)
|
BlankNote(Modifier, false)
|
||||||
} else {
|
} else {
|
||||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = multiSetCard.createdAt()) {
|
LaunchedEffect(key1 = multiSetCard.createdAt()) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead)
|
val newIsNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead)
|
||||||
|
|
||||||
NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt)
|
NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt)
|
||||||
|
|
||||||
|
if (newIsNew != isNew) {
|
||||||
|
isNew = newIsNew
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,32 +89,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
|||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (noteEvent is ChannelMessageEvent) {
|
routeFor(note, account.userProfile())?.let { navController.navigate(it) }
|
||||||
note
|
|
||||||
.channel()
|
|
||||||
?.let {
|
|
||||||
navController.navigate("Channel/${it.idHex}")
|
|
||||||
}
|
|
||||||
} else if (noteEvent is PrivateDmEvent) {
|
|
||||||
val replyAuthorBase =
|
|
||||||
(note.event as? PrivateDmEvent)
|
|
||||||
?.recipientPubKey()
|
|
||||||
?.let { LocalCache.getOrCreateUser(it) }
|
|
||||||
|
|
||||||
var userToComposeOn = note.author!!
|
|
||||||
|
|
||||||
if (replyAuthorBase != null) {
|
|
||||||
if (note.author == accountViewModel.userProfile()) {
|
|
||||||
userToComposeOn = replyAuthorBase
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navController.navigate("Room/${userToComposeOn.pubkeyHex}")
|
|
||||||
} else {
|
|
||||||
navController.navigate("Note/${note.idHex}") {
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
)
|
||||||
@@ -187,15 +168,10 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth()) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
Box(
|
Spacer(modifier = Modifier.width(65.dp))
|
||||||
modifier = Modifier
|
|
||||||
.width(65.dp)
|
|
||||||
.padding(0.dp)
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
NoteCompose(
|
NoteCompose(
|
||||||
baseNote = note,
|
baseNote = multiSetCard.note,
|
||||||
routeForLastRead = null,
|
routeForLastRead = null,
|
||||||
modifier = Modifier.padding(top = 5.dp),
|
modifier = Modifier.padding(top = 5.dp),
|
||||||
isBoostedNote = true,
|
isBoostedNote = true,
|
||||||
@@ -225,7 +201,7 @@ fun AuthorGalleryZaps(
|
|||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
authorNotes.forEach {
|
authorNotes.forEach {
|
||||||
AuthorPictureAndComment(it.key, it.value, navController, accountUser, accountViewModel)
|
AuthorPictureAndComment(it.key, navController, accountUser, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,7 +210,6 @@ fun AuthorGalleryZaps(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AuthorPictureAndComment(
|
private fun AuthorPictureAndComment(
|
||||||
zapRequest: Note,
|
zapRequest: Note,
|
||||||
zapEvent: Note,
|
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
accountUser: User,
|
accountUser: User,
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
@@ -242,8 +217,10 @@ private fun AuthorPictureAndComment(
|
|||||||
val author = zapRequest.author ?: return
|
val author = zapRequest.author ?: return
|
||||||
|
|
||||||
var content by remember { mutableStateOf<Pair<User, String?>>(Pair(author, null)) }
|
var content by remember { mutableStateOf<Pair<User, String?>>(Pair(author, null)) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(key1 = zapRequest.idHex) {
|
LaunchedEffect(key1 = zapRequest.idHex) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
(zapRequest.event as? LnZapRequestEvent)?.let {
|
(zapRequest.event as? LnZapRequestEvent)?.let {
|
||||||
val decryptedContent = accountViewModel.decryptZap(zapRequest)
|
val decryptedContent = accountViewModel.decryptZap(zapRequest)
|
||||||
if (decryptedContent != null) {
|
if (decryptedContent != null) {
|
||||||
@@ -256,6 +233,7 @@ private fun AuthorPictureAndComment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AuthorPictureAndComment(content.first, content.second, navController, accountUser, accountViewModel)
|
AuthorPictureAndComment(content.first, content.second, navController, accountUser, accountViewModel)
|
||||||
}
|
}
|
||||||
@@ -274,10 +252,14 @@ private fun AuthorPictureAndComment(
|
|||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
Row(
|
||||||
|
modifier = modifier.clickable {
|
||||||
|
navController.navigate("User/${author.pubkeyHex}")
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
FastNoteAuthorPicture(
|
FastNoteAuthorPicture(
|
||||||
author = author,
|
author = author,
|
||||||
navController = navController,
|
|
||||||
userAccount = accountUser,
|
userAccount = accountUser,
|
||||||
size = 35.dp
|
size = 35.dp
|
||||||
)
|
)
|
||||||
@@ -309,12 +291,16 @@ fun AuthorGallery(
|
|||||||
|
|
||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
authorNotes.forEach {
|
authorNotes.take(50).forEach {
|
||||||
val author = it.author
|
val author = it.author
|
||||||
if (author != null) {
|
if (author != null) {
|
||||||
AuthorPictureAndComment(author, null, navController, accountUser, accountViewModel)
|
AuthorPictureAndComment(author, null, navController, accountUser, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authorNotes.size > 50) {
|
||||||
|
Text(" and ${authorNotes.size - 50} others")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +308,6 @@ fun AuthorGallery(
|
|||||||
@Composable
|
@Composable
|
||||||
fun FastNoteAuthorPicture(
|
fun FastNoteAuthorPicture(
|
||||||
author: User,
|
author: User,
|
||||||
navController: NavController,
|
|
||||||
userAccount: User,
|
userAccount: User,
|
||||||
size: Dp,
|
size: Dp,
|
||||||
pictureModifier: Modifier = Modifier
|
pictureModifier: Modifier = Modifier
|
||||||
@@ -331,14 +316,12 @@ fun FastNoteAuthorPicture(
|
|||||||
val user = userState?.user ?: return
|
val user = userState?.user ?: return
|
||||||
|
|
||||||
val showFollowingMark = userAccount.isFollowingCached(user) || user === userAccount
|
val showFollowingMark = userAccount.isFollowingCached(user) || user === userAccount
|
||||||
|
|
||||||
UserPicture(
|
UserPicture(
|
||||||
userHex = user.pubkeyHex,
|
userHex = user.pubkeyHex,
|
||||||
userPicture = user.profilePicture(),
|
userPicture = user.profilePicture(),
|
||||||
showFollowingMark = showFollowingMark,
|
showFollowingMark = showFollowingMark,
|
||||||
size = size,
|
size = size,
|
||||||
modifier = pictureModifier,
|
modifier = pictureModifier
|
||||||
onClick = {
|
|
||||||
navController.navigate("User/${user.pubkeyHex}")
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,22 +92,36 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie
|
|||||||
val userState by baseUser.live().metadata.observeAsState()
|
val userState by baseUser.live().metadata.observeAsState()
|
||||||
val user = userState?.user ?: return
|
val user = userState?.user ?: return
|
||||||
|
|
||||||
|
user.nip05()?.let { nip05 ->
|
||||||
|
val parts = nip05.split("@")
|
||||||
|
if (parts.size == 2) {
|
||||||
|
val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex)
|
||||||
|
|
||||||
|
Column(modifier = columnModifier) {
|
||||||
|
DisplayNIP05(parts[0], parts[1], nip05Verified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DisplayNIP05(
|
||||||
|
user: String,
|
||||||
|
domain: String,
|
||||||
|
nip05Verified: Boolean?
|
||||||
|
) {
|
||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
|
|
||||||
user.nip05()?.let { nip05 ->
|
|
||||||
if (nip05.split("@").size == 2) {
|
|
||||||
Column(modifier = columnModifier) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
if (nip05.split("@")[0] != "_") {
|
if (user != "_") {
|
||||||
Text(
|
Text(
|
||||||
text = AnnotatedString(nip05.split("@")[0]),
|
text = AnnotatedString(user),
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex)
|
|
||||||
if (nip05Verified == null) {
|
if (nip05Verified == null) {
|
||||||
Icon(
|
Icon(
|
||||||
tint = Color.Yellow,
|
tint = Color.Yellow,
|
||||||
@@ -138,16 +152,13 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie
|
|||||||
}
|
}
|
||||||
|
|
||||||
ClickableText(
|
ClickableText(
|
||||||
text = AnnotatedString(nip05.split("@")[1]),
|
text = AnnotatedString(domain),
|
||||||
onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } },
|
onClick = { runCatching { uri.openUri("https://$domain") } },
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)),
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Visible
|
overflow = TextOverflow.Visible
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -92,6 +92,7 @@ private fun OptionNote(
|
|||||||
var optionTally by remember { mutableStateOf(Pair(BigDecimal.ZERO, defaultColor)) }
|
var optionTally by remember { mutableStateOf(Pair(BigDecimal.ZERO, defaultColor)) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = optionNumber, key2 = pollViewModel) {
|
LaunchedEffect(key1 = optionNumber, key2 = pollViewModel) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
val myTally = pollViewModel.optionVoteTally(optionNumber)
|
val myTally = pollViewModel.optionVoteTally(optionNumber)
|
||||||
val color = if (
|
val color = if (
|
||||||
pollViewModel.consensusThreshold != null &&
|
pollViewModel.consensusThreshold != null &&
|
||||||
@@ -106,6 +107,7 @@ private fun OptionNote(
|
|||||||
optionTally = Pair(myTally, color)
|
optionTally = Pair(myTally, color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ZapVote(
|
ZapVote(
|
||||||
baseNote,
|
baseNote,
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -319,21 +318,22 @@ fun ZapReaction(
|
|||||||
var zappingProgress by remember { mutableStateOf(0f) }
|
var zappingProgress by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
var wasZappedByLoggedInUser by remember { mutableStateOf(false) }
|
var wasZappedByLoggedInUser by remember { mutableStateOf(false) }
|
||||||
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
|
var zapAmountTxt by remember { mutableStateOf<String>("") }
|
||||||
|
|
||||||
LaunchedEffect(key1 = zapsState) {
|
LaunchedEffect(key1 = zapsState) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
if (!wasZappedByLoggedInUser) {
|
if (!wasZappedByLoggedInUser) {
|
||||||
wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
|
wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
|
||||||
}
|
}
|
||||||
|
|
||||||
zapAmount = account.calculateZappedAmount(zappedNote)
|
zapAmountTxt = showAmount(account.calculateZappedAmount(zappedNote))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = CenterVertically,
|
verticalAlignment = CenterVertically,
|
||||||
modifier = Modifier.size(iconSize)
|
modifier = Modifier
|
||||||
|
.size(iconSize)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
role = Role.Button,
|
role = Role.Button,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
@@ -456,7 +456,7 @@ fun ZapReaction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
showAmount(zapAmount),
|
zapAmountTxt,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = grayTint,
|
color = grayTint,
|
||||||
modifier = textModifier
|
modifier = textModifier
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -17,14 +18,24 @@ import androidx.navigation.NavController
|
|||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.*
|
import com.vitorpamplona.amethyst.model.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReplyInformation(replyTo: List<Note>?, mentions: List<String>, account: Account, navController: NavController) {
|
fun ReplyInformation(replyTo: List<Note>?, mentions: List<String>, account: Account, navController: NavController) {
|
||||||
val dupMentions = mentions.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
|
var dupMentions by remember { mutableStateOf<List<User>?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
dupMentions = mentions.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dupMentions != null) {
|
||||||
ReplyInformation(replyTo, dupMentions, account) {
|
ReplyInformation(replyTo, dupMentions, account) {
|
||||||
navController.navigate("User/${it.pubkeyHex}")
|
navController.navigate("User/${it.pubkeyHex}")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -102,6 +113,34 @@ fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Ac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<String>, channel: Channel, account: Account, navController: NavController) {
|
||||||
|
var sortedMentions by remember { mutableStateOf<List<User>?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
sortedMentions = mentions
|
||||||
|
.mapNotNull { LocalCache.checkGetOrCreateUser(it) }
|
||||||
|
.toSet()
|
||||||
|
.sortedBy { account.isFollowing(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortedMentions != null) {
|
||||||
|
ReplyInformationChannel(
|
||||||
|
replyTo,
|
||||||
|
sortedMentions,
|
||||||
|
channel,
|
||||||
|
onUserTagClick = {
|
||||||
|
navController.navigate("User/${it.pubkeyHex}")
|
||||||
|
},
|
||||||
|
onChannelTagClick = {
|
||||||
|
navController.navigate("Channel/${it.idHex}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<User>?, channel: Channel, navController: NavController) {
|
fun ReplyInformationChannel(replyTo: List<Note>?, mentions: List<User>?, channel: Channel, navController: NavController) {
|
||||||
ReplyInformationChannel(
|
ReplyInformationChannel(
|
||||||
|
|||||||
@@ -28,41 +28,55 @@ fun UsernameDisplay(baseUser: User, weight: 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
|
||||||
|
|
||||||
if (user.bestUsername() != null && user.bestDisplayName() != null) {
|
val bestUserName = user.bestUsername()
|
||||||
|
val bestDisplayName = user.bestDisplayName()
|
||||||
|
val npubDisplay = user.pubkeyDisplayHex()
|
||||||
|
|
||||||
|
UserNameDisplay(bestUserName, bestDisplayName, npubDisplay, weight)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UserNameDisplay(
|
||||||
|
bestUserName: String?,
|
||||||
|
bestDisplayName: String?,
|
||||||
|
npubDisplay: String,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
if (bestUserName != null && bestDisplayName != null) {
|
||||||
Text(
|
Text(
|
||||||
user.bestDisplayName() ?: "",
|
bestDisplayName,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"@${(user.bestUsername() ?: "")}",
|
"@$bestUserName",
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = weight
|
modifier = modifier
|
||||||
)
|
)
|
||||||
} else if (user.bestDisplayName() != null) {
|
} else if (bestDisplayName != null) {
|
||||||
Text(
|
Text(
|
||||||
user.bestDisplayName() ?: "",
|
bestDisplayName,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = weight
|
modifier = modifier
|
||||||
)
|
)
|
||||||
} else if (user.bestUsername() != null) {
|
} else if (bestUserName != null) {
|
||||||
Text(
|
Text(
|
||||||
"@${(user.bestUsername() ?: "")}",
|
"@$bestUserName",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = weight
|
modifier = modifier
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
user.pubkeyDisplayHex(),
|
npubDisplay,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = weight
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.UnfollowButton
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -95,7 +94,7 @@ fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewMode
|
|||||||
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
|
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = noteZap) {
|
LaunchedEffect(key1 = noteZap) {
|
||||||
withContext(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
zapAmount = (noteZap.event as? LnZapEvent)?.amount
|
zapAmount = (noteZap.event as? LnZapEvent)?.amount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import androidx.navigation.NavController
|
|||||||
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.service.model.ChannelMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ZapSetCard
|
import com.vitorpamplona.amethyst.ui.screen.ZapSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
@@ -70,15 +69,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.background(backgroundColor).combinedClickable(
|
modifier = Modifier.background(backgroundColor).combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (noteEvent !is ChannelMessageEvent) {
|
routeFor(note, account.userProfile())?.let { navController.navigate(it) }
|
||||||
navController.navigate("Note/${note.idHex}") {
|
|
||||||
launchSingleTop = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
note.channel()?.let {
|
|
||||||
navController.navigate("Channel/${it.idHex}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
)
|
||||||
@@ -113,7 +104,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor
|
|||||||
FlowRow() {
|
FlowRow() {
|
||||||
zapSetCard.zapEvents.forEach {
|
zapSetCard.zapEvents.forEach {
|
||||||
NoteAuthorPicture(
|
NoteAuthorPicture(
|
||||||
note = it.key,
|
baseNote = it.key,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
userAccount = account.userProfile(),
|
userAccount = account.userProfile(),
|
||||||
size = 35.dp
|
size = 35.dp
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
|
|||||||
FlowRow() {
|
FlowRow() {
|
||||||
zapSetCard.zapEvents.forEach {
|
zapSetCard.zapEvents.forEach {
|
||||||
NoteAuthorPicture(
|
NoteAuthorPicture(
|
||||||
note = it.key,
|
baseNote = it.key,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
userAccount = account.userProfile(),
|
userAccount = account.userProfile(),
|
||||||
size = 35.dp
|
size = 35.dp
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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.model.hexToByteArray
|
||||||
import com.vitorpamplona.amethyst.service.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClient
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
@@ -43,7 +43,7 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
fun startUI(key: String, useProxy: Boolean, proxyPort: Int) {
|
fun startUI(key: String, useProxy: Boolean, proxyPort: Int) {
|
||||||
val pattern = Pattern.compile(".+@.+\\.[a-z]+")
|
val pattern = Pattern.compile(".+@.+\\.[a-z]+")
|
||||||
val parsed = Nip19.uriToRoute(key)
|
val parsed = Nip19.uriToRoute(key)
|
||||||
val pubKeyParsed = parsed?.hex?.toByteArray()
|
val pubKeyParsed = parsed?.hex?.hexToByteArray()
|
||||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||||
|
|
||||||
val account =
|
val account =
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -32,6 +33,8 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
|||||||
import com.vitorpamplona.amethyst.ui.note.ZapSetCompose
|
import com.vitorpamplona.amethyst.ui.note.ZapSetCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose
|
import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
import kotlin.time.measureTimedValue
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -85,6 +88,7 @@ fun CardFeedView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun FeedLoaded(
|
private fun FeedLoaded(
|
||||||
state: CardFeedState.Loaded,
|
state: CardFeedState.Loaded,
|
||||||
@@ -114,6 +118,7 @@ private fun FeedLoaded(
|
|||||||
state = listState
|
state = listState
|
||||||
) {
|
) {
|
||||||
itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { _, item ->
|
itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { _, item ->
|
||||||
|
val (value, elapsed) = measureTimedValue {
|
||||||
when (item) {
|
when (item) {
|
||||||
is NoteCard -> NoteCompose(
|
is NoteCard -> NoteCompose(
|
||||||
item.note,
|
item.note,
|
||||||
@@ -170,5 +175,7 @@ private fun FeedLoaded(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Log.d("Time", "${item.javaClass.simpleName} Feed in $elapsed ${item.id()}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshFromOldState(newItems: Set<Note>) {
|
private fun refreshFromOldState(newItems: Set<Note>) {
|
||||||
val oldNotesState = _feedContent.value
|
val oldNotesState = _feedContent.value
|
||||||
|
|
||||||
val thisAccount = (localFilter as? NotificationFeedFilter)?.account
|
val thisAccount = (localFilter as? NotificationFeedFilter)?.account
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ private fun FeedLoaded(
|
|||||||
} else {
|
} else {
|
||||||
val replyAuthorBase =
|
val replyAuthorBase =
|
||||||
(note.event as? PrivateDmEvent)
|
(note.event as? PrivateDmEvent)
|
||||||
?.recipientPubKey()
|
?.verifiedRecipientPubKey()
|
||||||
?.let { LocalCache.getOrCreateUser(it) }
|
?.let { LocalCache.getOrCreateUser(it) }
|
||||||
|
|
||||||
var userToComposeOn = note.author!!
|
var userToComposeOn = note.author!!
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ fun NoteMaster(
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
NoteAuthorPicture(
|
NoteAuthorPicture(
|
||||||
note = baseNote,
|
baseNote = baseNote,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
userAccount = account.userProfile(),
|
userAccount = account.userProfile(),
|
||||||
size = 55.dp
|
size = 55.dp
|
||||||
|
|||||||
@@ -100,23 +100,42 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) {
|
fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
|
if (userId == null) return
|
||||||
|
|
||||||
|
var userBase by remember { mutableStateOf<User?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
userBase = LocalCache.checkGetOrCreateUser(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userBase?.let {
|
||||||
|
ProfileScreen(
|
||||||
|
user = it,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ProfileScreen(user: User, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
if (userId == null) return
|
UserProfileNewThreadFeedFilter.loadUserProfile(account, user)
|
||||||
|
UserProfileConversationsFeedFilter.loadUserProfile(account, user)
|
||||||
|
UserProfileFollowersFeedFilter.loadUserProfile(account, user)
|
||||||
|
UserProfileFollowsFeedFilter.loadUserProfile(account, user)
|
||||||
|
UserProfileZapsFeedFilter.loadUserProfile(user)
|
||||||
|
UserProfileReportsFeedFilter.loadUserProfile(user)
|
||||||
|
UserProfileBookmarksFeedFilter.loadUserProfile(account, user)
|
||||||
|
|
||||||
UserProfileNewThreadFeedFilter.loadUserProfile(account, userId)
|
NostrUserProfileDataSource.loadUserProfile(user)
|
||||||
UserProfileConversationsFeedFilter.loadUserProfile(account, userId)
|
|
||||||
UserProfileFollowersFeedFilter.loadUserProfile(account, userId)
|
|
||||||
UserProfileFollowsFeedFilter.loadUserProfile(account, userId)
|
|
||||||
UserProfileZapsFeedFilter.loadUserProfile(userId)
|
|
||||||
UserProfileReportsFeedFilter.loadUserProfile(userId)
|
|
||||||
UserProfileBookmarksFeedFilter.loadUserProfile(account, userId)
|
|
||||||
|
|
||||||
NostrUserProfileDataSource.loadUserProfile(userId)
|
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
@@ -124,7 +143,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
|
|||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
println("Profidle Start")
|
println("Profidle Start")
|
||||||
NostrUserProfileDataSource.loadUserProfile(userId)
|
NostrUserProfileDataSource.loadUserProfile(user)
|
||||||
NostrUserProfileDataSource.start()
|
NostrUserProfileDataSource.start()
|
||||||
}
|
}
|
||||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||||
|
|||||||
@@ -334,4 +334,10 @@
|
|||||||
<string name="upload_server_relays_nip95">Saját csomópontjaid (NIP-95)</string>
|
<string name="upload_server_relays_nip95">Saját csomópontjaid (NIP-95)</string>
|
||||||
<string name="upload_server_relays_nip95_explainer">A fájlokat a csomópontokra töltik fel és ott tárolják. Rögzített URL-től mentesek (harmadik féltől való függőség). Győződj meg róla, hogy legalább egy NIP-95 csomópont a csomópontlistában szerepel</string>
|
<string name="upload_server_relays_nip95_explainer">A fájlokat a csomópontokra töltik fel és ott tárolják. Rögzített URL-től mentesek (harmadik féltől való függőség). Győződj meg róla, hogy legalább egy NIP-95 csomópont a csomópontlistában szerepel</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<string name="follow_list_selection">Követek Lista</string>
|
||||||
|
<string name="follow_list_kind3follows">Mindenki akit követek</string>
|
||||||
|
<string name="follow_list_global">Globális</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -341,6 +341,10 @@
|
|||||||
<string name="upload_server_nostrbuild">nostr.build - trusted</string>
|
<string name="upload_server_nostrbuild">nostr.build - trusted</string>
|
||||||
<string name="upload_server_nostrbuild_explainer">Nostr.build can modify the file</string>
|
<string name="upload_server_nostrbuild_explainer">Nostr.build can modify the file</string>
|
||||||
|
|
||||||
|
<string name="upload_server_nostrfilesdev">nostrfiles.dev - trusted</string>
|
||||||
|
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev can modify the file</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="upload_server_imgur_nip94">Verifiable Imgur (NIP-94)</string>
|
<string name="upload_server_imgur_nip94">Verifiable Imgur (NIP-94)</string>
|
||||||
<string name="upload_server_imgur_nip94_explainer">Checks if Imgur modified the file. New NIP: other clients might not see it</string>
|
<string name="upload_server_imgur_nip94_explainer">Checks if Imgur modified the file. New NIP: other clients might not see it</string>
|
||||||
|
|
||||||
@@ -350,6 +354,10 @@
|
|||||||
<string name="upload_server_nostrbuild_nip94">Verifiable Nostr.build (NIP-94)</string>
|
<string name="upload_server_nostrbuild_nip94">Verifiable Nostr.build (NIP-94)</string>
|
||||||
<string name="upload_server_nostrbuild_nip94_explainer">Checks if Nostr.build modified the file. New NIP: other clients might not see it</string>
|
<string name="upload_server_nostrbuild_nip94_explainer">Checks if Nostr.build modified the file. New NIP: other clients might not see it</string>
|
||||||
|
|
||||||
|
<string name="upload_server_nostrfilesdev_nip94">Verifiable Nostrfiles.dev (NIP-94)</string>
|
||||||
|
<string name="upload_server_nostrfilesdev_nip94_explainer">Checks if Nostrfiles.dev modified the file. New NIP: other clients might not see it</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="upload_server_relays_nip95">Your relays (NIP-95)</string>
|
<string name="upload_server_relays_nip95">Your relays (NIP-95)</string>
|
||||||
<string name="upload_server_relays_nip95_explainer">Files are hosted by your relays. New NIP: check if they support</string>
|
<string name="upload_server_relays_nip95_explainer">Files are hosted by your relays. New NIP: check if they support</string>
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -4,6 +4,7 @@ import android.util.LruCache
|
|||||||
import com.google.android.gms.tasks.Task
|
import com.google.android.gms.tasks.Task
|
||||||
import com.google.android.gms.tasks.Tasks
|
import com.google.android.gms.tasks.Tasks
|
||||||
import com.google.mlkit.nl.languageid.LanguageIdentification
|
import com.google.mlkit.nl.languageid.LanguageIdentification
|
||||||
|
import com.google.mlkit.nl.languageid.LanguageIdentificationOptions
|
||||||
import com.google.mlkit.nl.translate.TranslateLanguage
|
import com.google.mlkit.nl.translate.TranslateLanguage
|
||||||
import com.google.mlkit.nl.translate.Translation
|
import com.google.mlkit.nl.translate.Translation
|
||||||
import com.google.mlkit.nl.translate.Translator
|
import com.google.mlkit.nl.translate.Translator
|
||||||
@@ -20,7 +21,8 @@ class ResultOrError(
|
|||||||
)
|
)
|
||||||
|
|
||||||
object LanguageTranslatorService {
|
object LanguageTranslatorService {
|
||||||
private val languageIdentification = LanguageIdentification.getClient()
|
private val options = LanguageIdentificationOptions.Builder().setConfidenceThreshold(0.6f).build()
|
||||||
|
private val languageIdentification = LanguageIdentification.getClient(options)
|
||||||
val lnRegex = Pattern.compile("\\blnbc[a-z0-9]+\\b", Pattern.CASE_INSENSITIVE)
|
val lnRegex = Pattern.compile("\\blnbc[a-z0-9]+\\b", Pattern.CASE_INSENSITIVE)
|
||||||
val tagRegex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)", Pattern.CASE_INSENSITIVE)
|
val tagRegex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)", Pattern.CASE_INSENSITIVE)
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -18,6 +18,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -34,7 +35,7 @@ import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
|||||||
import com.vitorpamplona.amethyst.service.lang.ResultOrError
|
import com.vitorpamplona.amethyst.service.lang.ResultOrError
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -57,8 +58,10 @@ fun TranslatableRichTextViewer(
|
|||||||
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
|
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(accountState) {
|
LaunchedEffect(accountState) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
LanguageTranslatorService.autoTranslate(
|
LanguageTranslatorService.autoTranslate(
|
||||||
content,
|
content,
|
||||||
account.dontTranslateFrom,
|
account.dontTranslateFrom,
|
||||||
@@ -70,8 +73,6 @@ fun TranslatableRichTextViewer(
|
|||||||
showOriginal = preference == task.result.sourceLang
|
showOriginal = preference == task.result.sourceLang
|
||||||
}
|
}
|
||||||
translatedTextState.value = task.result
|
translatedTextState.value = task.result
|
||||||
} else {
|
|
||||||
translatedTextState.value = ResultOrError(content, null, null, null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user