mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 15:44:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02786fb7d3 | ||
|
|
384594ba5c | ||
|
|
dab02f2cbd | ||
|
|
978f13fbf3 | ||
|
|
831386a656 | ||
|
|
8b7406e25a | ||
|
|
9e7fed6646 | ||
|
|
89d88326e8 | ||
|
|
f18dd02161 | ||
|
|
e00db19485 | ||
|
|
a706b71d7c | ||
|
|
a83b26cc6f | ||
|
|
dc73843447 | ||
|
|
3e6aa2b943 | ||
|
|
909d3f4fbe | ||
|
|
fb0748f4a7 | ||
|
|
1ec4823122 | ||
|
|
cce5e0bf21 | ||
|
|
0006595786 | ||
|
|
6b884f645e | ||
|
|
946723a80e | ||
|
|
791debbaca | ||
|
|
3bb5a4e50d | ||
|
|
3efa3a68fe | ||
|
|
dc314a94ff | ||
|
|
11b062c41f | ||
|
|
faeb2a3894 | ||
|
|
b1f45a346d | ||
|
|
0b16de6a85 | ||
|
|
66d4b75373 | ||
|
|
3b376a615c | ||
|
|
c8da570c89 | ||
|
|
38f4a5b7fc | ||
|
|
b03ff36cf3 | ||
|
|
d145938652 | ||
|
|
1993388d9f | ||
|
|
15e7540b26 | ||
|
|
c2beaf5f80 | ||
|
|
5d395419ff | ||
|
|
f672a51470 | ||
|
|
f741239e23 | ||
|
|
3442194b01 | ||
|
|
0b025d7679 |
+2
-2
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 297
|
||||
versionName "0.77.0"
|
||||
versionCode 302
|
||||
versionName "0.77.5"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.service.RichTextParser
|
||||
import com.vitorpamplona.amethyst.service.RichTextViewerState
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -666,7 +666,7 @@ class RichTextParserTest {
|
||||
|
||||
@Test
|
||||
fun testTextToParse() {
|
||||
val state = RichTextParser().parseText(textToParse, ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText(textToParse, EmptyTagList)
|
||||
Assert.assertEquals(
|
||||
"relay.shitforce.one, relayable.org, universe.nostrich.land, nos.lol, universe.nostrich.land?lang=zh, universe.nostrich.land?lang=en, relay.damus.io, relay.nostr.wirednet.jp, offchain.pub, nostr.rocks, relay.wellorder.net, nostr.oxtr.dev, universe.nostrich.land?lang=ja, relay.mostr.pub, nostr.bitcoiner.social, Nostr-Check.com, MR.Rabbit, Ancap.su, zapper.lol, smies.me, baller.hodl",
|
||||
state.urlSet.joinToString(", ")
|
||||
@@ -3989,7 +3989,7 @@ class RichTextParserTest {
|
||||
|
||||
@Test
|
||||
fun testShortTextToParse() {
|
||||
val state = RichTextParser().parseText("Hi, how are you doing? ", ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText("Hi, how are you doing? ", EmptyTagList)
|
||||
Assert.assertTrue(state.urlSet.isEmpty())
|
||||
Assert.assertTrue(state.imagesForPager.isEmpty())
|
||||
Assert.assertTrue(state.imageList.isEmpty())
|
||||
@@ -3999,7 +3999,7 @@ class RichTextParserTest {
|
||||
|
||||
@Test
|
||||
fun testShortNewLinesTextToParse() {
|
||||
val state = RichTextParser().parseText("\nHi, \nhow\n\n\n are you doing? \n", ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText("\nHi, \nhow\n\n\n are you doing? \n", EmptyTagList)
|
||||
Assert.assertTrue(state.urlSet.isEmpty())
|
||||
Assert.assertTrue(state.imagesForPager.isEmpty())
|
||||
Assert.assertTrue(state.imageList.isEmpty())
|
||||
@@ -4019,7 +4019,7 @@ See how it can be done here: https://lnshort.it/live-stream-embeds/
|
||||
https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg
|
||||
""".trimIndent()
|
||||
|
||||
val state = RichTextParser().parseText(text, ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText(text, EmptyTagList)
|
||||
Assert.assertEquals("https://lnshort.it/live-stream-embeds/", state.urlSet.firstOrNull())
|
||||
Assert.assertEquals("https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", state.imagesForPager.keys.firstOrNull())
|
||||
Assert.assertEquals("https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", state.imageList.firstOrNull()?.url)
|
||||
@@ -4080,7 +4080,7 @@ https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db
|
||||
fun testNewLineAfterImage() {
|
||||
val text = "That’s it ! That’s the #note https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg "
|
||||
|
||||
val state = RichTextParser().parseText(text, ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText(text, EmptyTagList)
|
||||
|
||||
printStateForDebug(state)
|
||||
|
||||
@@ -4106,7 +4106,7 @@ https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db
|
||||
fun testSapceAfterImage() {
|
||||
val text = "That’s it! https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg That’s the #note"
|
||||
|
||||
val state = RichTextParser().parseText(text, ImmutableListOfLists())
|
||||
val state = RichTextParser().parseText(text, EmptyTagList)
|
||||
|
||||
printStateForDebug(state)
|
||||
|
||||
|
||||
@@ -3,10 +3,13 @@ package com.vitorpamplona.amethyst
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.DefaultReactions
|
||||
import com.vitorpamplona.amethyst.model.DefaultZapAmounts
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.Nip47URI
|
||||
@@ -16,10 +19,8 @@ import com.vitorpamplona.amethyst.model.Settings
|
||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
@@ -76,7 +77,6 @@ private object PrefKeys {
|
||||
const val PREFERRED_LANGUAGE = "preferred_Language"
|
||||
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
|
||||
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
|
||||
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
|
||||
}
|
||||
|
||||
object LocalPreferences {
|
||||
@@ -325,11 +325,11 @@ object LocalPreferences {
|
||||
|
||||
val zapAmountChoices = getString(PrefKeys.ZAP_AMOUNTS, "[]")?.let {
|
||||
Event.mapper.readValue<List<Long>?>(it)
|
||||
}?.ifEmpty { listOf(500L, 1000L, 5000L) } ?: listOf(500L, 1000L, 5000L)
|
||||
}?.ifEmpty { DefaultZapAmounts } ?: DefaultZapAmounts
|
||||
|
||||
val reactionChoices = getString(PrefKeys.REACTION_CHOICES, "[]")?.let {
|
||||
Event.mapper.readValue<List<String>?>(it)
|
||||
}?.ifEmpty { listOf("+") } ?: listOf("+")
|
||||
}?.ifEmpty { DefaultReactions } ?: DefaultReactions
|
||||
|
||||
val defaultZapType = getString(PrefKeys.DEFAULT_ZAPTYPE, "")?.let { serverName ->
|
||||
LnZapEvent.ZapType.values().firstOrNull() { it.name == serverName }
|
||||
@@ -344,16 +344,22 @@ object LocalPreferences {
|
||||
Event.mapper.readValue<Nip47URI?>(it)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("LocalPreferences", "Error Decoding Zap Payment Request Server ${getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)}", e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
val latestContactList = try {
|
||||
getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let {
|
||||
Event.fromJson(it) as ContactListEvent?
|
||||
println("Decoding Contact List: " + it)
|
||||
if (it != null) {
|
||||
Event.fromJson(it) as ContactListEvent?
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
Log.w("LocalPreferences", "Error Decoding Contact List ${getString(PrefKeys.LATEST_CONTACT_LIST, null)}", e)
|
||||
null
|
||||
}
|
||||
|
||||
@@ -362,6 +368,7 @@ object LocalPreferences {
|
||||
Event.mapper.readValue<Map<String, String>?>(it)
|
||||
} ?: mapOf()
|
||||
} catch (e: Throwable) {
|
||||
Log.w("LocalPreferences", "Error Decoding Language Preferences ${getString(PrefKeys.LANGUAGE_PREFS, null)}", e)
|
||||
e.printStackTrace()
|
||||
mapOf()
|
||||
}
|
||||
@@ -387,6 +394,7 @@ object LocalPreferences {
|
||||
Event.mapper.readValue<Map<String, Long>?>(it)
|
||||
} ?: mapOf()
|
||||
} catch (e: Throwable) {
|
||||
Log.w("LocalPreferences", "Error Decoding Last Read per route ${getString(PrefKeys.LAST_READ_PER_ROUTE, null)}", e)
|
||||
e.printStackTrace()
|
||||
mapOf()
|
||||
}
|
||||
@@ -448,45 +456,4 @@ object LocalPreferences {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
fun migrateSingleUserPrefs() {
|
||||
if (currentAccount() != null) return
|
||||
|
||||
val pubkey = encryptedPreferences().getString(PrefKeys.NOSTR_PUBKEY, null) ?: return
|
||||
val npub = Hex.decode(pubkey).toNpub()
|
||||
|
||||
val stringPrefs = listOf(
|
||||
PrefKeys.NOSTR_PRIVKEY,
|
||||
PrefKeys.NOSTR_PUBKEY,
|
||||
PrefKeys.RELAYS,
|
||||
PrefKeys.LANGUAGE_PREFS,
|
||||
PrefKeys.TRANSLATE_TO,
|
||||
PrefKeys.ZAP_AMOUNTS,
|
||||
PrefKeys.LATEST_CONTACT_LIST
|
||||
)
|
||||
|
||||
val stringSetPrefs = listOf(
|
||||
PrefKeys.FOLLOWING_CHANNELS,
|
||||
PrefKeys.HIDDEN_USERS,
|
||||
PrefKeys.DONT_TRANSLATE_FROM
|
||||
)
|
||||
|
||||
encryptedPreferences().apply {
|
||||
val appPrefs = this
|
||||
encryptedPreferences(npub).edit().apply {
|
||||
val userPrefs = this
|
||||
|
||||
stringPrefs.forEach { userPrefs.putString(it, appPrefs.getString(it, null)) }
|
||||
stringSetPrefs.forEach { userPrefs.putStringSet(it, appPrefs.getStringSet(it, null)) }
|
||||
userPrefs.putBoolean(
|
||||
PrefKeys.HIDE_DELETE_REQUEST_DIALOG,
|
||||
appPrefs.getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
|
||||
)
|
||||
}.apply()
|
||||
}
|
||||
|
||||
encryptedPreferences().edit().clear().apply()
|
||||
addAccount(npub)
|
||||
updateCurrentAccount(npub)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ val DefaultChannels = setOf(
|
||||
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group
|
||||
)
|
||||
|
||||
val DefaultReactions = listOf("\uD83D\uDE80", "\uD83E\uDEC2", "\uD83D\uDC40", "\uD83D\uDE02")
|
||||
|
||||
val DefaultZapAmounts = listOf(500L, 1000L, 5000L)
|
||||
|
||||
fun getLanguagesSpokenByUser(): Set<String> {
|
||||
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration())
|
||||
val codedList = mutableSetOf<String>()
|
||||
@@ -67,8 +71,8 @@ class Account(
|
||||
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
|
||||
var languagePreferences: Map<String, String> = mapOf(),
|
||||
var translateTo: String = Locale.getDefault().language,
|
||||
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
||||
var reactionChoices: List<String> = listOf("+"),
|
||||
var zapAmountChoices: List<Long> = DefaultZapAmounts,
|
||||
var reactionChoices: List<String> = DefaultReactions,
|
||||
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE,
|
||||
var defaultFileServer: ServersAvailable = ServersAvailable.NOSTR_BUILD,
|
||||
var defaultHomeFollowList: String = KIND3_FOLLOWS,
|
||||
@@ -1198,7 +1202,7 @@ class Account(
|
||||
size = headerInfo.size.toString(),
|
||||
dimensions = headerInfo.dim,
|
||||
blurhash = headerInfo.blurHash,
|
||||
description = headerInfo.description,
|
||||
alt = headerInfo.alt,
|
||||
sensitiveContent = headerInfo.sensitiveContent,
|
||||
pubKey = keyPair.pubKey.toHexKey()
|
||||
)
|
||||
@@ -1230,7 +1234,7 @@ class Account(
|
||||
size = headerInfo.size.toString(),
|
||||
dimensions = headerInfo.dim,
|
||||
blurhash = headerInfo.blurHash,
|
||||
description = headerInfo.description,
|
||||
alt = headerInfo.alt,
|
||||
sensitiveContent = headerInfo.sensitiveContent,
|
||||
privateKey = keyPair.privKey!!
|
||||
)
|
||||
@@ -1269,7 +1273,7 @@ class Account(
|
||||
size = headerInfo.size.toString(),
|
||||
dimensions = headerInfo.dim,
|
||||
blurhash = headerInfo.blurHash,
|
||||
description = headerInfo.description,
|
||||
alt = headerInfo.alt,
|
||||
sensitiveContent = headerInfo.sensitiveContent,
|
||||
keyPair = keyPair
|
||||
)
|
||||
@@ -1294,7 +1298,7 @@ class Account(
|
||||
size = headerInfo.size.toString(),
|
||||
dimensions = headerInfo.dim,
|
||||
blurhash = headerInfo.blurHash,
|
||||
description = headerInfo.description,
|
||||
alt = headerInfo.alt,
|
||||
sensitiveContent = headerInfo.sensitiveContent,
|
||||
keyPair = keyPair
|
||||
)
|
||||
@@ -2474,13 +2478,7 @@ class Account(
|
||||
val privKey = keyPair.privKey
|
||||
|
||||
return if (listName != null) {
|
||||
val aTag = ATag(
|
||||
PeopleListEvent.kind,
|
||||
userProfile().pubkeyHex,
|
||||
listName,
|
||||
null
|
||||
).toTag()
|
||||
val list = LocalCache.addressables[aTag]
|
||||
val list = LocalCache.addressables[listName]
|
||||
if (list != null) {
|
||||
val publicHexList = (list.event as? PeopleListEvent)?.bookmarkedPeople() ?: emptySet()
|
||||
val privateHexList = privKey?.let {
|
||||
@@ -2503,13 +2501,7 @@ class Account(
|
||||
val privKey = keyPair.privKey
|
||||
|
||||
return if (listName != null) {
|
||||
val aTag = ATag(
|
||||
PeopleListEvent.kind,
|
||||
userProfile().pubkeyHex,
|
||||
listName,
|
||||
null
|
||||
).toTag()
|
||||
val list = LocalCache.addressables[aTag]
|
||||
val list = LocalCache.addressables[listName]
|
||||
if (list != null) {
|
||||
val publicAddresses = list.event?.hashtags() ?: emptySet()
|
||||
val privateAddresses = privKey?.let {
|
||||
@@ -2532,13 +2524,7 @@ class Account(
|
||||
val privKey = keyPair.privKey
|
||||
|
||||
return if (listName != null) {
|
||||
val aTag = ATag(
|
||||
PeopleListEvent.kind,
|
||||
userProfile().pubkeyHex,
|
||||
listName,
|
||||
null
|
||||
).toTag()
|
||||
val list = LocalCache.addressables[aTag]
|
||||
val list = LocalCache.addressables[listName]
|
||||
if (list != null) {
|
||||
val publicAddresses = list.event?.geohashes() ?: emptySet()
|
||||
val privateAddresses = privKey?.let {
|
||||
@@ -2561,13 +2547,7 @@ class Account(
|
||||
val privKey = keyPair.privKey
|
||||
|
||||
return if (listName != null) {
|
||||
val aTag = ATag(
|
||||
PeopleListEvent.kind,
|
||||
userProfile().pubkeyHex,
|
||||
listName,
|
||||
null
|
||||
).toTag()
|
||||
val list = LocalCache.addressables[aTag]
|
||||
val list = LocalCache.addressables[listName]
|
||||
if (list != null) {
|
||||
val publicAddresses = list.event?.taggedAddresses()?.map { it.toTag() } ?: emptySet()
|
||||
val privateAddresses = privKey?.let {
|
||||
|
||||
@@ -43,6 +43,10 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) {
|
||||
|
||||
return minOf(publishedAt, lastCreatedAt)
|
||||
}
|
||||
|
||||
fun dTag(): String? {
|
||||
return (event as? AddressableEvent)?.dTag()
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
||||
@@ -319,7 +319,7 @@ class User(val pubkeyHex: String) {
|
||||
return latestContactList?.verifiedFollowKeySet ?: emptySet()
|
||||
}
|
||||
|
||||
fun cachedFollowingTagSet(): Set<HexKey> {
|
||||
fun cachedFollowingTagSet(): Set<String> {
|
||||
return latestContactList?.verifiedFollowTagSet ?: emptySet()
|
||||
}
|
||||
|
||||
|
||||
@@ -209,6 +209,10 @@ object ExternalSignerUtils {
|
||||
if (it !== null) {
|
||||
if (it.moveToFirst()) {
|
||||
val index = it.getColumnIndex(columnName)
|
||||
if (index < 0) {
|
||||
Log.d("getDataFromResolver", "column '$columnName' not found")
|
||||
return null
|
||||
}
|
||||
return it.getString(index)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,16 +16,16 @@ class FileHeader(
|
||||
val size: Int,
|
||||
val dim: String?,
|
||||
val blurHash: String?,
|
||||
val description: String? = null,
|
||||
val alt: String? = null,
|
||||
val sensitiveContent: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
suspend fun prepare(fileUrl: String, mimeType: String?, description: String?, sensitiveContent: Boolean, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
||||
suspend fun prepare(fileUrl: String, mimeType: String?, alt: String?, sensitiveContent: Boolean, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
||||
try {
|
||||
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
|
||||
|
||||
if (imageData != null) {
|
||||
prepare(imageData, fileUrl, mimeType, description, sensitiveContent, onReady, onError)
|
||||
prepare(imageData, fileUrl, mimeType, alt, sensitiveContent, onReady, onError)
|
||||
} else {
|
||||
onError()
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class FileHeader(
|
||||
data: ByteArray,
|
||||
fileUrl: String,
|
||||
mimeType: String?,
|
||||
description: String?,
|
||||
alt: String?,
|
||||
sensitiveContent: Boolean,
|
||||
onReady: (FileHeader) -> Unit,
|
||||
onError: () -> Unit
|
||||
@@ -79,7 +79,7 @@ class FileHeader(
|
||||
Pair(null, null)
|
||||
}
|
||||
|
||||
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, description, sensitiveContent))
|
||||
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, alt, sensitiveContent))
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
||||
onError()
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
private const val ELEMENT_TAG_META = "meta"
|
||||
private const val ATTRIBUTE_VALUE_PROPERTY = "property"
|
||||
@@ -64,7 +65,11 @@ suspend fun getDocument(url: String, timeOut: Int = 30000): Document =
|
||||
withContext(Dispatchers.IO) {
|
||||
val request: Request = Request.Builder().url(url).get().build()
|
||||
val html = HttpClient.getHttpClient().newCall(request).execute().use {
|
||||
it.body.string()
|
||||
if (it.isSuccessful) {
|
||||
it.body.string()
|
||||
} else {
|
||||
throw IllegalArgumentException("Website returned: " + it.code)
|
||||
}
|
||||
}
|
||||
|
||||
Jsoup.parse(html)
|
||||
|
||||
@@ -56,7 +56,6 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
LocalPreferences.migrateSingleUserPrefs()
|
||||
val language = LocalPreferences.getPreferredLanguage()
|
||||
if (language.isNotBlank()) {
|
||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(language)
|
||||
|
||||
@@ -26,7 +26,7 @@ open class NewMediaModel : ViewModel() {
|
||||
var mediaType by mutableStateOf<String?>(null)
|
||||
|
||||
var selectedServer by mutableStateOf<ServersAvailable?>(null)
|
||||
var description by mutableStateOf("")
|
||||
var alt by mutableStateOf("")
|
||||
var sensitiveContent by mutableStateOf(false)
|
||||
|
||||
// Images and Videos
|
||||
@@ -79,7 +79,7 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingPercentage.value = 0.2f
|
||||
uploadingDescription.value = "Loading"
|
||||
contentResolver.openInputStream(fileUri)?.use {
|
||||
createNIP95Record(it.readBytes(), contentType, description, sensitiveContent, relayList = relayList)
|
||||
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
|
||||
}
|
||||
?: run {
|
||||
viewModelScope.launch {
|
||||
@@ -103,7 +103,7 @@ open class NewMediaModel : ViewModel() {
|
||||
createNIP94Record(
|
||||
imageUrl,
|
||||
mimeType,
|
||||
description,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
relayList = relayList
|
||||
)
|
||||
@@ -139,7 +139,7 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingDescription.value = null
|
||||
uploadingPercentage.value = 0.0f
|
||||
|
||||
description = ""
|
||||
alt = ""
|
||||
selectedServer = account?.defaultFileServer
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ open class NewMediaModel : ViewModel() {
|
||||
return !isUploadingImage && galleryUri != null && selectedServer != null
|
||||
}
|
||||
|
||||
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
fun createNIP94Record(imageUrl: String, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
uploadingPercentage.value = 0.40f
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
uploadingDescription.value = "Server Processing"
|
||||
@@ -166,7 +166,7 @@ open class NewMediaModel : ViewModel() {
|
||||
imageData,
|
||||
imageUrl,
|
||||
mimeType,
|
||||
description,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
uploadingPercentage.value = 0.90f
|
||||
@@ -200,7 +200,7 @@ open class NewMediaModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
uploadingPercentage.value = 0.30f
|
||||
uploadingDescription.value = "Hashing"
|
||||
|
||||
@@ -209,7 +209,7 @@ open class NewMediaModel : ViewModel() {
|
||||
bytes,
|
||||
"",
|
||||
mimeType,
|
||||
description,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
uploadingDescription.value = "Signing"
|
||||
|
||||
@@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -186,8 +187,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
||||
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
||||
)
|
||||
|
||||
val fileServerOptions = remember { fileServers.map { it.second }.toImmutableList() }
|
||||
val fileServerExplainers = remember { fileServers.map { it.third }.toImmutableList() }
|
||||
val fileServerOptions = remember { fileServers.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
|
||||
val resolver = LocalContext.current.contentResolver
|
||||
|
||||
Row(
|
||||
@@ -252,7 +252,6 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
||||
label = stringResource(id = R.string.file_server),
|
||||
placeholder = fileServers.firstOrNull { it.first == accountViewModel.account.defaultFileServer }?.second ?: fileServers[0].second,
|
||||
options = fileServerOptions,
|
||||
explainers = fileServerExplainers,
|
||||
onSelect = {
|
||||
postViewModel.selectedServer = fileServers[it].first
|
||||
},
|
||||
@@ -288,8 +287,8 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
||||
value = postViewModel.description,
|
||||
onValueChange = { postViewModel.description = it },
|
||||
value = postViewModel.alt,
|
||||
onValueChange = { postViewModel.alt = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.content_description_example),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
@@ -13,7 +13,8 @@ class NewMessageTagger(
|
||||
var message: String,
|
||||
var mentions: List<User>? = null,
|
||||
var replyTos: List<Note>? = null,
|
||||
var channelHex: String? = null
|
||||
var channelHex: String? = null,
|
||||
var dao: Dao
|
||||
) {
|
||||
|
||||
val directMentions = mutableSetOf<HexKey>()
|
||||
@@ -40,20 +41,20 @@ class NewMessageTagger(
|
||||
return (if (channelHex != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
|
||||
}
|
||||
|
||||
fun run() {
|
||||
suspend fun run() {
|
||||
// adds all references to mentions and reply tos
|
||||
message.split('\n').forEach { paragraph: String ->
|
||||
paragraph.split(' ').forEach { word: String ->
|
||||
val results = parseDirtyWordForKey(word)
|
||||
|
||||
if (results?.key?.type == Nip19.Type.USER) {
|
||||
addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
|
||||
addUserToMentions(dao.getOrCreateUser(results.key.hex))
|
||||
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
||||
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
|
||||
addNoteToReplyTos(dao.getOrCreateNote(results.key.hex))
|
||||
} else if (results?.key?.type == Nip19.Type.EVENT) {
|
||||
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
|
||||
addNoteToReplyTos(dao.getOrCreateNote(results.key.hex))
|
||||
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
|
||||
val note = dao.checkGetOrCreateAddressableNote(results.key.hex)
|
||||
if (note != null) {
|
||||
addNoteToReplyTos(note)
|
||||
}
|
||||
@@ -66,19 +67,19 @@ class NewMessageTagger(
|
||||
paragraph.split(' ').map { word: String ->
|
||||
val results = parseDirtyWordForKey(word)
|
||||
if (results?.key?.type == Nip19.Type.USER) {
|
||||
val user = LocalCache.getOrCreateUser(results.key.hex)
|
||||
val user = dao.getOrCreateUser(results.key.hex)
|
||||
|
||||
"nostr:${user.pubkeyNpub()}${results.restOfWord}"
|
||||
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
||||
val note = LocalCache.getOrCreateNote(results.key.hex)
|
||||
val note = dao.getOrCreateNote(results.key.hex)
|
||||
|
||||
"nostr:${note.toNEvent()}${results.restOfWord}"
|
||||
} else if (results?.key?.type == Nip19.Type.EVENT) {
|
||||
val note = LocalCache.getOrCreateNote(results.key.hex)
|
||||
val note = dao.getOrCreateNote(results.key.hex)
|
||||
|
||||
"nostr:${note.toNEvent()}${results.restOfWord}"
|
||||
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
|
||||
val note = dao.checkGetOrCreateAddressableNote(results.key.hex)
|
||||
if (note != null) {
|
||||
"nostr:${note.idNote()}${results.restOfWord}"
|
||||
} else {
|
||||
@@ -91,6 +92,7 @@ class NewMessageTagger(
|
||||
}.joinToString("\n")
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class DirtyKeyInfo(val key: Nip19.Return, val restOfWord: String)
|
||||
|
||||
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
|
||||
@@ -155,3 +157,9 @@ class NewMessageTagger(
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface Dao {
|
||||
suspend fun getOrCreateUser(hex: String): User
|
||||
suspend fun getOrCreateNote(hex: String): Note
|
||||
suspend fun checkGetOrCreateAddressableNote(hex: String): Note?
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
@@ -142,7 +143,7 @@ fun NewPostView(
|
||||
var relayList = account.activeWriteRelays()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
postViewModel.load(account, baseReplyTo, quote)
|
||||
postViewModel.load(accountViewModel, baseReplyTo, quote)
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
postViewModel.imageUploadingError.collect { error ->
|
||||
@@ -211,10 +212,8 @@ fun NewPostView(
|
||||
}
|
||||
PostButton(
|
||||
onPost = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postViewModel.sendPost(relayList = relayList)
|
||||
onClose()
|
||||
}
|
||||
postViewModel.sendPost(relayList = relayList)
|
||||
onClose()
|
||||
},
|
||||
isActive = postViewModel.canPost()
|
||||
)
|
||||
@@ -339,8 +338,8 @@ fun NewPostView(
|
||||
ImageVideoDescription(
|
||||
url,
|
||||
account.defaultFileServer,
|
||||
onAdd = { description, server, sensitiveContent ->
|
||||
postViewModel.upload(url, description, sensitiveContent, server, context, relayList)
|
||||
onAdd = { alt, server, sensitiveContent ->
|
||||
postViewModel.upload(url, alt, sensitiveContent, server, context, relayList)
|
||||
account.changeDefaultFileServer(server)
|
||||
},
|
||||
onCancel = {
|
||||
@@ -1320,8 +1319,7 @@ fun ImageVideoDescription(
|
||||
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
||||
)
|
||||
|
||||
val fileServerOptions = remember { fileServers.map { it.second }.toImmutableList() }
|
||||
val fileServerExplainers = remember { fileServers.map { it.third }.toImmutableList() }
|
||||
val fileServerOptions = remember { fileServers.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
|
||||
|
||||
var selectedServer by remember { mutableStateOf(defaultServer) }
|
||||
var message by remember { mutableStateOf("") }
|
||||
@@ -1435,7 +1433,6 @@ fun ImageVideoDescription(
|
||||
label = stringResource(id = R.string.file_server),
|
||||
placeholder = fileServers.filter { it.first == defaultServer }.firstOrNull()?.second ?: fileServers[0].second,
|
||||
options = fileServerOptions,
|
||||
explainers = fileServerExplainers,
|
||||
onSelect = {
|
||||
selectedServer = fileServers[it].first
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.ui.components.Split
|
||||
import com.vitorpamplona.amethyst.ui.components.isValidURL
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
@@ -45,6 +46,7 @@ enum class UserSuggestionAnchor {
|
||||
|
||||
@Stable
|
||||
open class NewPostViewModel() : ViewModel() {
|
||||
var accountViewModel: AccountViewModel? = null
|
||||
var account: Account? = null
|
||||
var requiresNIP24: Boolean = false
|
||||
|
||||
@@ -115,7 +117,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
// NIP24 Wrapped DMs / Group messages
|
||||
var nip24 by mutableStateOf(false)
|
||||
|
||||
open fun load(account: Account, replyingTo: Note?, quote: Note?) {
|
||||
open fun load(accountViewModel: AccountViewModel, replyingTo: Note?, quote: Note?) {
|
||||
originalNote = replyingTo
|
||||
replyingTo?.let { replyNote ->
|
||||
if (replyNote.event is BaseTextNoteEvent) {
|
||||
@@ -147,8 +149,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
|
||||
canAddInvoice = account.userProfile().info?.lnAddress() != null
|
||||
canAddZapRaiser = account.userProfile().info?.lnAddress() != null
|
||||
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
|
||||
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
|
||||
contentToAddUrl = null
|
||||
|
||||
@@ -160,14 +162,26 @@ open class NewPostViewModel() : ViewModel() {
|
||||
forwardZapTo = Split()
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
|
||||
this.account = account
|
||||
this.accountViewModel = accountViewModel
|
||||
this.account = accountViewModel.account
|
||||
}
|
||||
|
||||
fun sendPost(relayList: List<Relay>? = null) {
|
||||
val tagger = NewMessageTagger(message.text, mentions, replyTos, originalNote?.channelHex())
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
innerSendPost(relayList)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun innerSendPost(relayList: List<Relay>? = null) {
|
||||
if (accountViewModel == null) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
val tagger = NewMessageTagger(message.text, mentions, replyTos, originalNote?.channelHex(), accountViewModel!!)
|
||||
tagger.run()
|
||||
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null)
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null, accountViewModel!!)
|
||||
toUsersTagger.run()
|
||||
val dmUsers = toUsersTagger.mentions
|
||||
|
||||
@@ -285,7 +299,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
cancel()
|
||||
}
|
||||
|
||||
fun upload(galleryUri: Uri, description: String, sensitiveContent: Boolean, server: ServersAvailable, context: Context, relayList: List<Relay>? = null) {
|
||||
fun upload(galleryUri: Uri, alt: String, sensitiveContent: Boolean, server: ServersAvailable, context: Context, relayList: List<Relay>? = null) {
|
||||
isUploadingImage = true
|
||||
contentToAddUrl = null
|
||||
|
||||
@@ -300,7 +314,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
onReady = { fileUri, contentType, size ->
|
||||
if (server == ServersAvailable.NIP95) {
|
||||
contentResolver.openInputStream(fileUri)?.use {
|
||||
createNIP95Record(it.readBytes(), contentType, description, sensitiveContent, relayList = relayList)
|
||||
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
|
||||
}
|
||||
} else {
|
||||
ImageUploader.uploadImage(
|
||||
@@ -311,7 +325,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
if (isNIP94Server(server)) {
|
||||
createNIP94Record(imageUrl, mimeType, description, sensitiveContent)
|
||||
createNIP94Record(imageUrl, mimeType, alt, sensitiveContent)
|
||||
} else {
|
||||
isUploadingImage = false
|
||||
message = TextFieldValue(message.text + "\n\n" + imageUrl)
|
||||
@@ -523,13 +537,13 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun createNIP94Record(imageUrl: String, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
fun createNIP94Record(imageUrl: String, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Images don't seem to be ready immediately after upload
|
||||
FileHeader.prepare(
|
||||
imageUrl,
|
||||
mimeType,
|
||||
description,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
val note = account?.sendHeader(it, relayList = relayList)
|
||||
@@ -554,13 +568,13 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, description: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
FileHeader.prepare(
|
||||
bytes,
|
||||
"",
|
||||
mimeType,
|
||||
description,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
val nip95 = account?.createNip95(bytes, headerInfo = it)
|
||||
|
||||
@@ -248,7 +248,7 @@ private fun DisplayOwnerInformation(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadUser(baseUserHex = userHex) {
|
||||
LoadUser(baseUserHex = userHex, accountViewModel) {
|
||||
Crossfade(it) {
|
||||
if (it != null) {
|
||||
UserCompose(baseUser = it, accountViewModel = accountViewModel, showDiviser = false, nav = nav)
|
||||
|
||||
@@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun NewCommunityNoteButton(communityIdHex: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LoadNote(baseNoteHex = communityIdHex) {
|
||||
LoadNote(baseNoteHex = communityIdHex, accountViewModel) {
|
||||
it?.let {
|
||||
NewCommunityNoteButton(it, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@@ -40,12 +40,12 @@ import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NIP30Parser
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
@@ -61,20 +61,21 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun ClickableRoute(
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (nip19.type) {
|
||||
Nip19.Type.USER -> {
|
||||
DisplayUser(nip19, nav)
|
||||
DisplayUser(nip19, accountViewModel, nav)
|
||||
}
|
||||
Nip19.Type.ADDRESS -> {
|
||||
DisplayAddress(nip19, nav)
|
||||
DisplayAddress(nip19, accountViewModel, nav)
|
||||
}
|
||||
Nip19.Type.NOTE -> {
|
||||
DisplayNote(nip19, nav)
|
||||
DisplayNote(nip19, accountViewModel, nav)
|
||||
}
|
||||
Nip19.Type.EVENT -> {
|
||||
DisplayEvent(nip19, nav)
|
||||
DisplayEvent(nip19, accountViewModel, nav)
|
||||
}
|
||||
else -> {
|
||||
Text(
|
||||
@@ -86,34 +87,15 @@ fun ClickableRoute(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadNote(
|
||||
hex: String,
|
||||
content: @Composable (Note) -> Unit
|
||||
) {
|
||||
var noteBase by remember(hex) { mutableStateOf(LocalCache.getNoteIfExists(hex)) }
|
||||
|
||||
if (noteBase == null) {
|
||||
LaunchedEffect(key1 = hex) {
|
||||
launch(Dispatchers.IO) {
|
||||
noteBase = LocalCache.checkGetOrCreateNote(hex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
noteBase?.let {
|
||||
content(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayEvent(
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadNote(nip19.hex) {
|
||||
LoadNote(nip19.hex, accountViewModel) {
|
||||
if (it != null) {
|
||||
DisplayNoteLink(it, nip19, nav)
|
||||
DisplayNoteLink(it, nip19, accountViewModel, nav)
|
||||
} else {
|
||||
CreateClickableText(
|
||||
clickablePart = remember(nip19) { "@${nip19.hex.toShortenHex()}" },
|
||||
@@ -128,11 +110,12 @@ private fun DisplayEvent(
|
||||
@Composable
|
||||
private fun DisplayNote(
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadNote(nip19.hex) {
|
||||
LoadNote(nip19.hex, accountViewModel = accountViewModel) {
|
||||
if (it != null) {
|
||||
DisplayNoteLink(it, nip19, nav)
|
||||
DisplayNoteLink(it, nip19, accountViewModel, nav)
|
||||
} else {
|
||||
CreateClickableText(
|
||||
clickablePart = remember(nip19) { "@${nip19.hex.toShortenHex()}" },
|
||||
@@ -148,6 +131,7 @@ private fun DisplayNote(
|
||||
private fun DisplayNoteLink(
|
||||
it: Note,
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteState by it.live().metadata.observeAsState()
|
||||
@@ -177,7 +161,7 @@ private fun DisplayNoteLink(
|
||||
nav = nav
|
||||
)
|
||||
} else if (channelHex != null) {
|
||||
LoadChannel(baseChannelHex = channelHex) { baseChannel ->
|
||||
LoadChannel(baseChannelHex = channelHex, accountViewModel) { baseChannel ->
|
||||
val channelState by baseChannel.live.observeAsState()
|
||||
val channelDisplayName by remember(channelState) {
|
||||
derivedStateOf {
|
||||
@@ -205,14 +189,15 @@ private fun DisplayNoteLink(
|
||||
@Composable
|
||||
private fun DisplayAddress(
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var noteBase by remember(nip19) { mutableStateOf(LocalCache.getNoteIfExists(nip19.hex)) }
|
||||
var noteBase by remember(nip19) { mutableStateOf(accountViewModel.getNoteIfExists(nip19.hex)) }
|
||||
|
||||
if (noteBase == null) {
|
||||
LaunchedEffect(key1 = nip19.hex) {
|
||||
launch(Dispatchers.IO) {
|
||||
noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
|
||||
accountViewModel.checkGetOrCreateAddressableNote(nip19.hex) {
|
||||
noteBase = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,14 +229,19 @@ private fun DisplayAddress(
|
||||
@Composable
|
||||
private fun DisplayUser(
|
||||
nip19: Nip19.Return,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var userBase by remember(nip19) { mutableStateOf(LocalCache.getUserIfExists(nip19.hex)) }
|
||||
var userBase by remember(nip19) {
|
||||
mutableStateOf(
|
||||
accountViewModel.getUserIfExists(nip19.hex)
|
||||
)
|
||||
}
|
||||
|
||||
if (userBase == null) {
|
||||
LaunchedEffect(key1 = nip19.hex) {
|
||||
launch(Dispatchers.IO) {
|
||||
userBase = LocalCache.checkGetOrCreateUser(nip19.hex)
|
||||
accountViewModel.checkGetOrCreateUser(nip19.hex) {
|
||||
userBase = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ private fun RenderWordWithoutPreview(
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
is BechSegment -> BechLink(word.segmentText, false, backgroundColor, accountViewModel, nav)
|
||||
is HashTagSegment -> HashTag(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, false, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> NormalWord(word.segmentText, style)
|
||||
@@ -270,7 +270,7 @@ private fun RenderWordWithPreview(
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
|
||||
is HashTagSegment -> HashTag(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, true, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> NormalWord(word.segmentText, style)
|
||||
@@ -654,7 +654,7 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: MutableState<Co
|
||||
}
|
||||
} else if (loadedLink?.nip19 != null) {
|
||||
Row() {
|
||||
ClickableRoute(loadedLink?.nip19!!, nav)
|
||||
ClickableRoute(loadedLink?.nip19!!, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
val text = remember {
|
||||
@@ -763,8 +763,8 @@ private fun InlineIcon(hashtagIcon: HashtagIcon) =
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TagLink(word: HashIndexUserSegment, nav: (String) -> Unit) {
|
||||
LoadUser(baseUserHex = word.hex) {
|
||||
fun TagLink(word: HashIndexUserSegment, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LoadUser(baseUserHex = word.hex, accountViewModel) {
|
||||
if (it == null) {
|
||||
Text(text = word.segmentText)
|
||||
} else {
|
||||
@@ -776,15 +776,15 @@ fun TagLink(word: HashIndexUserSegment, nav: (String) -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadNote(baseNoteHex: String, content: @Composable (Note?) -> Unit) {
|
||||
fun LoadNote(baseNoteHex: String, accountViewModel: AccountViewModel, content: @Composable (Note?) -> Unit) {
|
||||
var note by remember(baseNoteHex) {
|
||||
mutableStateOf<Note?>(LocalCache.getNoteIfExists(baseNoteHex))
|
||||
mutableStateOf<Note?>(accountViewModel.getNoteIfExists(baseNoteHex))
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
LaunchedEffect(key1 = baseNoteHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
note = LocalCache.checkGetOrCreateNote(baseNoteHex)
|
||||
accountViewModel.checkGetOrCreateNote(baseNoteHex) {
|
||||
note = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -794,7 +794,7 @@ fun LoadNote(baseNoteHex: String, content: @Composable (Note?) -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun TagLink(word: HashIndexEventSegment, canPreview: Boolean, backgroundColor: MutableState<Color>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LoadNote(baseNoteHex = word.hex) {
|
||||
LoadNote(baseNoteHex = word.hex, accountViewModel) {
|
||||
if (it == null) {
|
||||
Text(text = remember { word.segmentText.toShortenHex() })
|
||||
} else {
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -37,8 +38,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
fun TextSpinner(
|
||||
label: String?,
|
||||
placeholder: String,
|
||||
options: ImmutableList<String>,
|
||||
explainers: ImmutableList<String>? = null,
|
||||
options: ImmutableList<TitleExplainer>,
|
||||
onSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -74,8 +74,8 @@ fun TextSpinner(
|
||||
|
||||
if (optionsShowing) {
|
||||
options.isNotEmpty().also {
|
||||
SpinnerSelectionDialog(options = options, explainers = explainers, onDismiss = { optionsShowing = false }) {
|
||||
currentText = options[it]
|
||||
SpinnerSelectionDialog(options = options, onDismiss = { optionsShowing = false }) {
|
||||
currentText = options[it].title
|
||||
optionsShowing = false
|
||||
onSelect(it)
|
||||
}
|
||||
@@ -85,10 +85,40 @@ fun TextSpinner(
|
||||
|
||||
@Composable
|
||||
fun SpinnerSelectionDialog(
|
||||
options: ImmutableList<String>,
|
||||
explainers: ImmutableList<String>?,
|
||||
options: ImmutableList<TitleExplainer>,
|
||||
onDismiss: () -> Unit,
|
||||
onSelect: (Int) -> Unit
|
||||
) {
|
||||
SpinnerSelectionDialog(
|
||||
options = options,
|
||||
onSelect = onSelect,
|
||||
onDismiss = onDismiss
|
||||
) { item ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = item.title, color = MaterialTheme.colors.onSurface)
|
||||
}
|
||||
item.explainer?.let {
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(text = it, color = Color.Gray, fontSize = Font14SP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> SpinnerSelectionDialog(
|
||||
options: ImmutableList<T>,
|
||||
onSelect: (Int) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onRenderItem: @Composable (T) -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
@@ -106,23 +136,7 @@ fun SpinnerSelectionDialog(
|
||||
}
|
||||
) {
|
||||
Column() {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(text = item, color = MaterialTheme.colors.onSurface)
|
||||
}
|
||||
explainers?.getOrNull(index)?.let {
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(text = it, color = Color.Gray, fontSize = Font14SP)
|
||||
}
|
||||
}
|
||||
onRenderItem(item)
|
||||
}
|
||||
}
|
||||
if (index < options.lastIndex) {
|
||||
@@ -133,3 +147,6 @@ fun SpinnerSelectionDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class TitleExplainer(val title: String, val explainer: String? = null)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class TranslationConfig(
|
||||
val result: String?,
|
||||
val sourceLang: String?,
|
||||
val targetLang: String?,
|
||||
val showOriginal: Boolean
|
||||
)
|
||||
@@ -62,7 +62,7 @@ fun UrlPreviewCard(
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
.padding(start = 10.dp, end = 10.dp, top = 5.dp),
|
||||
color = Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -612,7 +613,7 @@ fun Waveform(
|
||||
DrawWaveform(waveform, waveformProgress, modifier)
|
||||
|
||||
val restartFlow = remember {
|
||||
mutableStateOf(0)
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
// Keeps the screen on while playing and viewing videos.
|
||||
|
||||
@@ -477,14 +477,16 @@ private fun AddedImageFeatures(
|
||||
if (content.blurhash != null) {
|
||||
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
|
||||
} else {
|
||||
FlowRow() {
|
||||
FlowRow(Modifier.fillMaxWidth()) {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
ClickableUrl(urlText = "${content.url} ", url = content.url)
|
||||
FlowRow(Modifier.fillMaxWidth()) {
|
||||
ClickableUrl(urlText = "${content.url} ", url = content.url)
|
||||
}
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -54,11 +55,13 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import coil.Coil
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
@@ -88,6 +91,7 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadCityName
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.note.LongCommunityHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
|
||||
@@ -117,6 +121,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -227,7 +232,7 @@ private fun CommunityTopBar(
|
||||
nav: (String) -> Unit,
|
||||
navPopBack: () -> Unit
|
||||
) {
|
||||
LoadAddressableNote(aTagHex = id) { baseNote ->
|
||||
LoadAddressableNote(aTagHex = id, accountViewModel) { baseNote ->
|
||||
if (baseNote != null) {
|
||||
FlexibleTopBarWithBackButton(
|
||||
title = {
|
||||
@@ -288,7 +293,7 @@ private fun RenderRoomTopBar(
|
||||
if (room.users.size == 1) {
|
||||
FlexibleTopBarWithBackButton(
|
||||
title = {
|
||||
LoadUser(baseUserHex = room.users.first()) { baseUser ->
|
||||
LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser ->
|
||||
if (baseUser != null) {
|
||||
ClickableUserPicture(
|
||||
baseUser = baseUser,
|
||||
@@ -303,7 +308,7 @@ private fun RenderRoomTopBar(
|
||||
}
|
||||
},
|
||||
extendableRow = {
|
||||
LoadUser(baseUserHex = room.users.first()) {
|
||||
LoadUser(baseUserHex = room.users.first(), accountViewModel) {
|
||||
if (it != null) {
|
||||
UserCompose(
|
||||
baseUser = it,
|
||||
@@ -348,7 +353,7 @@ private fun ChannelTopBar(
|
||||
nav: (String) -> Unit,
|
||||
navPopBack: () -> Unit
|
||||
) {
|
||||
LoadChannel(baseChannelHex = id) { baseChannel ->
|
||||
LoadChannel(baseChannelHex = id, accountViewModel) { baseChannel ->
|
||||
FlexibleTopBarWithBackButton(
|
||||
prefixRow = {
|
||||
if (baseChannel is LiveActivitiesChannel) {
|
||||
@@ -382,11 +387,12 @@ fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState
|
||||
val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
followLists,
|
||||
list,
|
||||
true
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
withGlobal = true,
|
||||
withRoutes = false
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultStoriesFollowList(listName)
|
||||
accountViewModel.account.changeDefaultStoriesFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,11 +403,16 @@ fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, a
|
||||
val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
followLists,
|
||||
list,
|
||||
true
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
withGlobal = true,
|
||||
withRoutes = true
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultHomeFollowList(listName)
|
||||
if (listName.type == CodeNameType.ROUTE) {
|
||||
nav(listName.code)
|
||||
} else {
|
||||
accountViewModel.account.changeDefaultHomeFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -412,11 +423,12 @@ fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: Scaffold
|
||||
val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
followLists,
|
||||
list,
|
||||
true
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
withGlobal = true,
|
||||
withRoutes = false
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultNotificationFollowList(listName)
|
||||
accountViewModel.account.changeDefaultNotificationFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,11 +439,12 @@ fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldSta
|
||||
val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
followLists,
|
||||
list,
|
||||
true
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
withGlobal = true,
|
||||
withRoutes = false
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultDiscoveryFollowList(listName)
|
||||
accountViewModel.account.changeDefaultDiscoveryFollowList(listName.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -523,37 +536,81 @@ private fun LoggedInUserPictureDrawer(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowList(followListsModel: FollowListViewModel, listName: String, withGlobal: Boolean, onChange: (String) -> Unit) {
|
||||
val kind3Follow = Pair(KIND3_FOLLOWS, stringResource(id = R.string.follow_list_kind3follows))
|
||||
val globalFollow = Pair(GLOBAL_FOLLOWS, stringResource(id = R.string.follow_list_global))
|
||||
fun FollowList(
|
||||
followListsModel: FollowListViewModel,
|
||||
listName: String,
|
||||
withGlobal: Boolean,
|
||||
withRoutes: Boolean,
|
||||
onChange: (CodeName) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val kind3Follow = CodeName(KIND3_FOLLOWS, ResourceName(R.string.follow_list_kind3follows, context), CodeNameType.HARDCODED)
|
||||
val globalFollow = CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global, context), CodeNameType.HARDCODED)
|
||||
|
||||
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
|
||||
|
||||
val followLists by followListsModel.followLists.collectAsState()
|
||||
val followLists by followListsModel.peopleLists.collectAsState()
|
||||
val routeList by followListsModel.routes.collectAsState()
|
||||
|
||||
val allLists = remember(followLists) {
|
||||
(defaultOptions + followLists)
|
||||
if (withRoutes) {
|
||||
(defaultOptions + followLists + routeList)
|
||||
} else {
|
||||
(defaultOptions + followLists)
|
||||
}
|
||||
}
|
||||
|
||||
val followNames by remember(followLists) {
|
||||
derivedStateOf {
|
||||
allLists.map { it.second }.toImmutableList()
|
||||
allLists.map { it.name }.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
SimpleTextSpinner(
|
||||
placeholder = allLists.firstOrNull { it.first == listName }?.second ?: "Select an Option",
|
||||
placeholder = allLists.firstOrNull { it.code == listName }?.name?.name() ?: "Select an Option",
|
||||
options = followNames,
|
||||
onSelect = {
|
||||
onChange(allLists.getOrNull(it)?.first ?: KIND3_FOLLOWS)
|
||||
onChange(allLists.getOrNull(it) ?: kind3Follow)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
enum class CodeNameType {
|
||||
HARDCODED, PEOPLE_LIST, ROUTE
|
||||
}
|
||||
|
||||
abstract class Name {
|
||||
abstract fun name(): String
|
||||
}
|
||||
|
||||
class GeoHashName(val geoHashTag: String) : Name() {
|
||||
override fun name() = "/g/$geoHashTag"
|
||||
}
|
||||
class HashtagName(val hashTag: String) : Name() {
|
||||
override fun name() = "#$hashTag"
|
||||
}
|
||||
class ResourceName(val resourceId: Int, val context: Context) : Name() {
|
||||
override fun name() = context.getString(resourceId)
|
||||
}
|
||||
|
||||
class PeopleListName(val note: AddressableNote) : Name() {
|
||||
override fun name() = note.dTag() ?: ""
|
||||
}
|
||||
class CommunityName(val note: AddressableNote) : Name() {
|
||||
override fun name() = "/n/${(note.dTag() ?: "")}"
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class CodeName(val code: String, val name: Name, val type: CodeNameType)
|
||||
|
||||
@Stable
|
||||
class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
private var _followLists = MutableStateFlow<ImmutableList<Pair<String, String>>>(emptyList<Pair<String, String>>().toPersistentList())
|
||||
val followLists = _followLists.asStateFlow()
|
||||
private var _peopleLists = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
|
||||
val peopleLists = _peopleLists.asStateFlow()
|
||||
|
||||
private var _routes = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
|
||||
val routes = _routes.asStateFlow()
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
@@ -571,14 +628,34 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
event.pubKey == account.userProfile().pubkeyHex &&
|
||||
(event.tags.size > 1 || event.content.length > 50)
|
||||
) {
|
||||
Pair(event.dTag(), event.dTag())
|
||||
CodeName(event.address().toTag(), PeopleListName(it.value), CodeNameType.PEOPLE_LIST)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.sortedBy { it.second }.toImmutableList()
|
||||
}.sortedBy { it.name.name() }.toImmutableList()
|
||||
|
||||
if (!equalImmutableLists(_followLists.value, newFollowLists)) {
|
||||
_followLists.emit(newFollowLists)
|
||||
if (!equalImmutableLists(_peopleLists.value, newFollowLists)) {
|
||||
_peopleLists.emit(newFollowLists)
|
||||
}
|
||||
|
||||
val communities = account.userProfile().cachedFollowingCommunitiesSet().mapNotNull {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
||||
CodeName("Community/${communityNote.idHex}", CommunityName(communityNote), CodeNameType.ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
val hashtags = account.userProfile().cachedFollowingTagSet().map {
|
||||
CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE)
|
||||
}
|
||||
|
||||
val geotags = account.userProfile().cachedFollowingGeohashSet().map {
|
||||
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
|
||||
}
|
||||
|
||||
val routeList = (communities + hashtags + geotags).sortedBy { it.name.name() }.toImmutableList()
|
||||
|
||||
if (!equalImmutableLists(_routes.value, routeList)) {
|
||||
_routes.emit(routeList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +667,10 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
checkNotInMainThread()
|
||||
if (newNotes.any { it.event is PeopleListEvent }) {
|
||||
if (newNotes.any {
|
||||
it.event?.pubKey() == account.userProfile().pubkeyHex && (it.event is PeopleListEvent || it.event is ContactListEvent)
|
||||
}
|
||||
) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
@@ -612,8 +692,7 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
@Composable
|
||||
fun SimpleTextSpinner(
|
||||
placeholder: String,
|
||||
options: ImmutableList<String>,
|
||||
explainers: ImmutableList<String>? = null,
|
||||
options: ImmutableList<Name>,
|
||||
onSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -649,10 +728,68 @@ fun SimpleTextSpinner(
|
||||
|
||||
if (optionsShowing) {
|
||||
options.isNotEmpty().also {
|
||||
SpinnerSelectionDialog(options = options, explainers = explainers, onDismiss = { optionsShowing = false }) {
|
||||
currentText = options[it]
|
||||
optionsShowing = false
|
||||
onSelect(it)
|
||||
SpinnerSelectionDialog(
|
||||
options = options,
|
||||
onDismiss = { optionsShowing = false },
|
||||
onSelect = {
|
||||
currentText = options[it].name()
|
||||
optionsShowing = false
|
||||
onSelect(it)
|
||||
}
|
||||
) {
|
||||
RenderOption(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderOption(it: Name) {
|
||||
when (it) {
|
||||
is GeoHashName -> {
|
||||
LoadCityName(it.geoHashTag) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = "/g/$it", color = MaterialTheme.colors.onSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
is HashtagName -> {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = it.name(), color = MaterialTheme.colors.onSurface)
|
||||
}
|
||||
}
|
||||
is ResourceName -> {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = stringResource(id = it.resourceId), color = MaterialTheme.colors.onSurface)
|
||||
}
|
||||
}
|
||||
is PeopleListName -> {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = it.name(), color = MaterialTheme.colors.onSurface)
|
||||
}
|
||||
}
|
||||
is CommunityName -> {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val name by it.note.live().metadata.map {
|
||||
"/n/" + (it.note as? AddressableNote)?.dTag()
|
||||
}.observeAsState()
|
||||
|
||||
Text(text = name ?: "", color = MaterialTheme.colors.onSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
LoadStatuses(user = baseAccountUser) { statuses ->
|
||||
LoadStatuses(user = baseAccountUser, accountViewModel) { statuses ->
|
||||
if (statuses.isEmpty()) {
|
||||
val currentStatus = remember {
|
||||
mutableStateOf("")
|
||||
|
||||
@@ -150,6 +150,14 @@ sealed class Route(
|
||||
route = "Settings",
|
||||
icon = R.drawable.ic_settings
|
||||
)
|
||||
|
||||
companion object {
|
||||
val InvertedLayouts = setOf(
|
||||
Channel.route,
|
||||
Room.route,
|
||||
RoomByAuthor.route
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// **
|
||||
|
||||
@@ -646,7 +646,7 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
|
||||
fun RenderChannelThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteEvent = baseNote.event as? ChannelCreateEvent ?: return
|
||||
|
||||
LoadChannel(baseChannelHex = baseNote.idHex) {
|
||||
LoadChannel(baseChannelHex = baseNote.idHex, accountViewModel) {
|
||||
RenderChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ import androidx.lifecycle.map
|
||||
import com.patrykandpatrick.vico.core.extension.forEachIndexedExtended
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
@@ -99,7 +98,7 @@ fun ChatroomComposeChannelOrUser(
|
||||
}
|
||||
|
||||
if (channelHex != null) {
|
||||
ChatroomChannel(channelHex, baseNote, accountViewModel, nav)
|
||||
ChatroomChannel(channelHex!!, baseNote, accountViewModel, nav)
|
||||
} else {
|
||||
ChatroomPrivateMessages(baseNote, accountViewModel, nav)
|
||||
}
|
||||
@@ -134,12 +133,12 @@ private fun ChatroomPrivateMessages(
|
||||
|
||||
@Composable
|
||||
private fun ChatroomChannel(
|
||||
channelHex: HexKey?,
|
||||
channelHex: HexKey,
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadChannel(baseChannelHex = channelHex!!) { channel ->
|
||||
LoadChannel(baseChannelHex = channelHex, accountViewModel) { channel ->
|
||||
ChannelRoomCompose(baseNote, channel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
@@ -277,7 +276,7 @@ private fun UserRoomCompose(
|
||||
)
|
||||
},
|
||||
channelTitle = {
|
||||
RoomNameDisplay(room, it, accountViewModel.userProfile())
|
||||
RoomNameDisplay(room, it, accountViewModel)
|
||||
},
|
||||
channelLastTime = createAt,
|
||||
channelLastContent = content,
|
||||
@@ -287,20 +286,20 @@ private fun UserRoomCompose(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RoomNameDisplay(room: ChatroomKey, modifier: Modifier, loggedInUser: User) {
|
||||
val roomSubject by loggedInUser.live().messages.map {
|
||||
fun RoomNameDisplay(room: ChatroomKey, modifier: Modifier, accountViewModel: AccountViewModel) {
|
||||
val roomSubject by accountViewModel.userProfile().live().messages.map {
|
||||
it.user.privateChatrooms[room]?.subject
|
||||
}.distinctUntilChanged().observeAsState(loggedInUser.privateChatrooms[room]?.subject)
|
||||
}.distinctUntilChanged().observeAsState(accountViewModel.userProfile().privateChatrooms[room]?.subject)
|
||||
|
||||
Crossfade(targetState = roomSubject, modifier) {
|
||||
if (it != null && it.isNotBlank()) {
|
||||
if (room.users.size > 1) {
|
||||
DisplayRoomSubject(it)
|
||||
} else {
|
||||
DisplayUserAndSubject(room.users.first(), it)
|
||||
DisplayUserAndSubject(room.users.first(), it, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
DisplayUserSetAsSubject(room)
|
||||
DisplayUserSetAsSubject(room, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,7 +307,8 @@ fun RoomNameDisplay(room: ChatroomKey, modifier: Modifier, loggedInUser: User) {
|
||||
@Composable
|
||||
private fun DisplayUserAndSubject(
|
||||
user: HexKey,
|
||||
subject: String
|
||||
subject: String,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Row() {
|
||||
Text(
|
||||
@@ -322,7 +322,7 @@ private fun DisplayUserAndSubject(
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1
|
||||
)
|
||||
LoadUser(baseUserHex = user) {
|
||||
LoadUser(baseUserHex = user, accountViewModel = accountViewModel) {
|
||||
it?.let {
|
||||
UsernameDisplay(it, Modifier.weight(1f))
|
||||
}
|
||||
@@ -333,6 +333,7 @@ private fun DisplayUserAndSubject(
|
||||
@Composable
|
||||
fun DisplayUserSetAsSubject(
|
||||
room: ChatroomKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
fontWeight: FontWeight = FontWeight.Bold
|
||||
) {
|
||||
val userList = remember(room) {
|
||||
@@ -342,7 +343,7 @@ fun DisplayUserSetAsSubject(
|
||||
if (userList.size == 1) {
|
||||
// Regular Design
|
||||
Row() {
|
||||
LoadUser(baseUserHex = userList[0]) {
|
||||
LoadUser(baseUserHex = userList[0], accountViewModel) {
|
||||
it?.let {
|
||||
UsernameDisplay(it, Modifier.weight(1f), fontWeight = fontWeight)
|
||||
}
|
||||
@@ -351,7 +352,7 @@ fun DisplayUserSetAsSubject(
|
||||
} else {
|
||||
Row() {
|
||||
userList.take(4).forEachIndexedExtended { index, isFirst, isLast, value ->
|
||||
LoadUser(baseUserHex = value) {
|
||||
LoadUser(baseUserHex = value, accountViewModel) {
|
||||
it?.let {
|
||||
ShortUsernameDisplay(baseUser = it, fontWeight = fontWeight)
|
||||
}
|
||||
@@ -416,15 +417,14 @@ private fun WatchNotificationChanges(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadUser(baseUserHex: String, content: @Composable (User?) -> Unit) {
|
||||
fun LoadUser(baseUserHex: String, accountViewModel: AccountViewModel, content: @Composable (User?) -> Unit) {
|
||||
var user by remember(baseUserHex) {
|
||||
mutableStateOf(LocalCache.getUserIfExists(baseUserHex))
|
||||
mutableStateOf(accountViewModel.getUserIfExists(baseUserHex))
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
LaunchedEffect(key1 = baseUserHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newUser = LocalCache.checkGetOrCreateUser(baseUserHex)
|
||||
accountViewModel.checkGetOrCreateUser(baseUserHex) { newUser ->
|
||||
if (user != newUser) {
|
||||
user = newUser
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -66,6 +67,7 @@ import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
@@ -285,7 +287,7 @@ fun NormalChatNote(
|
||||
modifier = modif,
|
||||
horizontalArrangement = alignment
|
||||
) {
|
||||
val availableBubbleSize = remember { mutableStateOf(0) }
|
||||
val availableBubbleSize = remember { mutableIntStateOf(0) }
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val modif2 = remember {
|
||||
@@ -355,7 +357,7 @@ private fun RenderBubble(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val bubbleSize = remember { mutableStateOf(0) }
|
||||
val bubbleSize = remember { mutableIntStateOf(0) }
|
||||
|
||||
val bubbleModifier = remember {
|
||||
Modifier
|
||||
@@ -626,7 +628,7 @@ private fun RenderRegularTextNote(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
||||
val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) }
|
||||
val modifier = remember { Modifier.padding(top = 5.dp) }
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -33,6 +30,7 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -70,8 +68,9 @@ import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -172,7 +171,6 @@ private fun Galeries(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } }
|
||||
val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } }
|
||||
val likeEvents by remember { derivedStateOf { multiSetCard.likeEventsByType } }
|
||||
|
||||
@@ -181,6 +179,16 @@ private fun Galeries(
|
||||
val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } }
|
||||
|
||||
if (hasZapEvents) {
|
||||
var zapEvents by remember(multiSetCard.zapEvents) {
|
||||
mutableStateOf<ImmutableList<ZapAmountCommentNotification>>(persistentListOf())
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
accountViewModel.decryptAmountMessageInGroup(multiSetCard.zapEvents) {
|
||||
zapEvents = it
|
||||
}
|
||||
}
|
||||
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
@@ -250,7 +258,7 @@ fun RenderLikeGallery(
|
||||
|
||||
@Composable
|
||||
fun RenderZapGallery(
|
||||
zapEvents: ImmutableList<CombinedZap>,
|
||||
zapEvents: ImmutableList<ZapAmountCommentNotification>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
@@ -297,10 +305,29 @@ fun RenderBoostGallery(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MapZaps(
|
||||
zaps: ImmutableList<CombinedZap>,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (ImmutableList<ZapAmountCommentNotification>) -> Unit
|
||||
) {
|
||||
var zapEvents by remember(zaps) {
|
||||
mutableStateOf<ImmutableList<ZapAmountCommentNotification>>(persistentListOf())
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
accountViewModel.decryptAmountMessageInGroup(zaps) {
|
||||
zapEvents = it
|
||||
}
|
||||
}
|
||||
|
||||
content(zapEvents)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun AuthorGalleryZaps(
|
||||
authorNotes: ImmutableList<CombinedZap>,
|
||||
authorNotes: ImmutableList<ZapAmountCommentNotification>,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
@@ -308,24 +335,12 @@ fun AuthorGalleryZaps(
|
||||
Column(modifier = StdStartPadding) {
|
||||
FlowRow() {
|
||||
authorNotes.forEach {
|
||||
ParseAuthorCommentAndAmount(it, backgroundColor, nav, accountViewModel)
|
||||
RenderState(it, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ParseAuthorCommentAndAmount(
|
||||
zap: CombinedZap,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
ParseAuthorCommentAndAmount(zap.request, zap.response, accountViewModel) { state ->
|
||||
RenderState(state, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class ZapAmountCommentNotification(
|
||||
val user: User?,
|
||||
@@ -361,15 +376,15 @@ private fun ParseAuthorCommentAndAmount(
|
||||
onReady(content)
|
||||
}
|
||||
|
||||
fun click(content: MutableState<ZapAmountCommentNotification>, nav: (String) -> Unit) {
|
||||
content.value.user?.let {
|
||||
fun click(content: ZapAmountCommentNotification, nav: (String) -> Unit) {
|
||||
content.user?.let {
|
||||
nav(routeFor(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderState(
|
||||
content: MutableState<ZapAmountCommentNotification>,
|
||||
content: ZapAmountCommentNotification,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -401,97 +416,65 @@ val commentTextSize = 12.sp
|
||||
|
||||
@Composable
|
||||
private fun DisplayAuthorCommentAndAmount(
|
||||
authorComment: MutableState<ZapAmountCommentNotification>,
|
||||
authorComment: ZapAmountCommentNotification,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
Box(modifier = Size35Modifier, contentAlignment = Alignment.BottomCenter) {
|
||||
CrossfadeToDisplayPicture(authorComment, accountViewModel)
|
||||
CrossfadeToDisplayAmount(authorComment)
|
||||
}
|
||||
|
||||
CrossfadeToDisplayComment(authorComment, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CrossfadeToDisplayPicture(authorComment: MutableState<ZapAmountCommentNotification>, accountViewModel: AccountViewModel) {
|
||||
Crossfade(authorComment.value) {
|
||||
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(it.user, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CrossfadeToDisplayAmount(authorComment: MutableState<ZapAmountCommentNotification>) {
|
||||
val visible by remember(authorComment) {
|
||||
derivedStateOf {
|
||||
authorComment.value.amount != null
|
||||
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(authorComment.user, accountViewModel)
|
||||
authorComment.amount?.let {
|
||||
CrossfadeToDisplayAmount(it)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
authorComment.comment?.let {
|
||||
CrossfadeToDisplayComment(it, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CrossfadeToDisplayAmount(amount: String) {
|
||||
Box(
|
||||
modifier = amountBoxModifier,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
authorComment.value.amount?.let {
|
||||
Box(
|
||||
modifier = amountBoxModifier,
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
val backgroundColor = MaterialTheme.colors.overPictureBackground
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(Size35dp)
|
||||
.background(backgroundColor)
|
||||
},
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.bitcoinColor,
|
||||
fontSize = commentTextSize,
|
||||
modifier = bottomPadding1dp
|
||||
)
|
||||
}
|
||||
}
|
||||
val backgroundColor = MaterialTheme.colors.overPictureBackground
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(Size35dp)
|
||||
.background(backgroundColor)
|
||||
},
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = amount,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.bitcoinColor,
|
||||
fontSize = commentTextSize,
|
||||
modifier = bottomPadding1dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CrossfadeToDisplayComment(
|
||||
authorComment: MutableState<ZapAmountCommentNotification>,
|
||||
comment: String,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val visible by remember(authorComment) {
|
||||
derivedStateOf {
|
||||
authorComment.value.comment != null
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
authorComment.value.comment?.let {
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
tags = remember { ImmutableListOfLists() },
|
||||
modifier = textBoxModifier,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
TranslatableRichTextViewer(
|
||||
content = comment,
|
||||
canPreview = true,
|
||||
tags = EmptyTagList,
|
||||
modifier = textBoxModifier,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@@ -543,25 +526,25 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
WatchUserMetadata(author) { baseUserPicture ->
|
||||
Crossfade(targetState = baseUserPicture) { userPicture ->
|
||||
RobohashAsyncImageProxy(
|
||||
robot = author.pubkeyHex,
|
||||
model = userPicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = MaterialTheme.colors.profile35dpModifier,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
// Crossfade(targetState = baseUserPicture) { userPicture ->
|
||||
RobohashAsyncImageProxy(
|
||||
robot = author.pubkeyHex,
|
||||
model = baseUserPicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = MaterialTheme.colors.profile35dpModifier,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
// }
|
||||
}
|
||||
|
||||
WatchUserFollows(author.pubkeyHex, accountViewModel) { isFollowing ->
|
||||
Crossfade(targetState = isFollowing) {
|
||||
if (it) {
|
||||
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
|
||||
FollowingIcon(Size10dp)
|
||||
}
|
||||
// Crossfade(targetState = isFollowing) {
|
||||
if (isFollowing) {
|
||||
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
|
||||
FollowingIcon(Size10dp)
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ fun ObserveDisplayNip05Status(
|
||||
) {
|
||||
val nip05 by baseUser.live().nip05Changes.observeAsState(baseUser.nip05())
|
||||
|
||||
LoadStatuses(baseUser) { statuses ->
|
||||
LoadStatuses(baseUser, accountViewModel) { statuses ->
|
||||
Crossfade(targetState = nip05, modifier = columnModifier, label = "ObserveDisplayNip05StatusCrossfade") {
|
||||
VerifyAndDisplayNIP05OrStatusLine(it, statuses, baseUser, columnModifier, accountViewModel, nav)
|
||||
}
|
||||
@@ -233,7 +233,7 @@ fun DisplayStatus(
|
||||
)
|
||||
}
|
||||
} else if (nostrATag != null) {
|
||||
LoadAddressableNote(nostrATag) { note ->
|
||||
LoadAddressableNote(nostrATag, accountViewModel) { note ->
|
||||
if (note != null) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
IconButton(
|
||||
@@ -255,7 +255,7 @@ fun DisplayStatus(
|
||||
}
|
||||
}
|
||||
} else if (nostrHexID != null) {
|
||||
LoadNote(baseNoteHex = nostrHexID) {
|
||||
LoadNote(baseNoteHex = nostrHexID, accountViewModel) {
|
||||
if (it != null) {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
IconButton(
|
||||
|
||||
@@ -54,6 +54,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Alignment.Companion.TopStart
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
@@ -84,7 +85,6 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.RelayBriefInfo
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -135,11 +135,12 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
@@ -152,13 +153,13 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.UserNameRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyBackground
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.repostProfileBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
@@ -179,12 +180,12 @@ import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiUrl
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.HighlightEvent
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
@@ -631,7 +632,7 @@ fun LongCommunityHeader(
|
||||
TranslatableRichTextViewer(
|
||||
content = summary ?: stringResource(id = R.string.community_no_descriptor),
|
||||
canPreview = false,
|
||||
tags = remember { ImmutableListOfLists(emptyList()) },
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = background,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -673,14 +674,13 @@ fun LongCommunityHeader(
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = noteState) {
|
||||
launch(Dispatchers.IO) {
|
||||
val noteEvent = (noteState?.note?.event as? CommunityDefinitionEvent)
|
||||
val newParticipantUsers = noteEvent?.moderators()?.mapNotNull { part ->
|
||||
LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) }
|
||||
}?.toImmutableList()
|
||||
val participants = (noteState?.note?.event as? CommunityDefinitionEvent)?.moderators()
|
||||
|
||||
if (newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers)) {
|
||||
participantUsers = newParticipantUsers
|
||||
if (participants != null) {
|
||||
accountViewModel.loadParticipants(participants) { newParticipantUsers ->
|
||||
if (newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers)) {
|
||||
participantUsers = newParticipantUsers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1042,7 +1042,9 @@ private fun NoteBody(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(3.dp))
|
||||
if (baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent) {
|
||||
Spacer(modifier = Modifier.height(3.dp))
|
||||
}
|
||||
|
||||
if (!makeItShort) {
|
||||
ReplyRow(
|
||||
@@ -1342,7 +1344,7 @@ fun RenderTextEvent(
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
val modifier = remember(note) { Modifier.fillMaxWidth() }
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent,
|
||||
@@ -1381,7 +1383,7 @@ fun RenderPoll(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
} else {
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
@@ -1556,7 +1558,7 @@ fun RenderAppDefinition(
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
|
||||
) {
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
val bgColor = MaterialTheme.colors.background
|
||||
val backgroundColor = remember {
|
||||
mutableStateOf(bgColor)
|
||||
@@ -1629,7 +1631,7 @@ private fun RenderPrivateMessage(
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
if (eventContent != null) {
|
||||
if (makeItShort && isAuthorTheLoggedUser) {
|
||||
@@ -1669,7 +1671,7 @@ private fun RenderPrivateMessage(
|
||||
),
|
||||
canPreview = !makeItShort,
|
||||
Modifier.fillMaxWidth(),
|
||||
ImmutableListOfLists(),
|
||||
EmptyTagList,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
@@ -1814,9 +1816,8 @@ fun DisplayPeopleList(
|
||||
) {
|
||||
val noteEvent = baseNote.event as? PeopleListEvent ?: return
|
||||
|
||||
var members by remember { mutableStateOf<List<User>>(listOf()) }
|
||||
var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) }
|
||||
|
||||
val account = accountViewModel.userProfile()
|
||||
var expanded by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
@@ -1845,10 +1846,8 @@ fun DisplayPeopleList(
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
members = noteEvent.bookmarkedPeople().mapNotNull { hex ->
|
||||
LocalCache.checkGetOrCreateUser(hex)
|
||||
}.sortedBy { account.isFollowing(it) }.reversed()
|
||||
accountViewModel.loadUsers(noteEvent.bookmarkedPeople()) {
|
||||
members = it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1896,15 +1895,11 @@ private fun RenderBadgeAward(
|
||||
val noteEvent = note.event as? BadgeAwardEvent ?: return
|
||||
var awardees by remember { mutableStateOf<List<User>>(listOf()) }
|
||||
|
||||
val account = accountViewModel.userProfile()
|
||||
|
||||
Text(text = stringResource(R.string.award_granted_to))
|
||||
|
||||
LaunchedEffect(key1 = note) {
|
||||
launch(Dispatchers.IO) {
|
||||
awardees = noteEvent.awardees().mapNotNull { hex ->
|
||||
LocalCache.checkGetOrCreateUser(hex)
|
||||
}.sortedBy { account.isFollowing(it) }.reversed()
|
||||
accountViewModel.loadUsers(noteEvent.awardees()) {
|
||||
awardees = it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2013,7 +2008,7 @@ fun RenderPostApproval(
|
||||
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
noteEvent.communities().forEach {
|
||||
LoadAddressableNote(it) {
|
||||
LoadAddressableNote(it, accountViewModel) {
|
||||
it?.let {
|
||||
NoteCompose(
|
||||
it,
|
||||
@@ -2052,15 +2047,14 @@ fun RenderPostApproval(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadAddressableNote(aTagHex: String, content: @Composable (AddressableNote?) -> Unit) {
|
||||
fun LoadAddressableNote(aTagHex: String, accountViewModel: AccountViewModel, content: @Composable (AddressableNote?) -> Unit) {
|
||||
var note by remember(aTagHex) {
|
||||
mutableStateOf<AddressableNote?>(LocalCache.getAddressableNoteIfExists(aTagHex))
|
||||
mutableStateOf<AddressableNote?>(accountViewModel.getAddressableNoteIfExists(aTagHex))
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
LaunchedEffect(key1 = aTagHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newNote = LocalCache.checkGetOrCreateAddressableNote(aTagHex)
|
||||
accountViewModel.checkGetOrCreateAddressableNote(aTagHex) { newNote ->
|
||||
if (newNote != note) {
|
||||
note = newNote
|
||||
}
|
||||
@@ -2072,15 +2066,14 @@ fun LoadAddressableNote(aTagHex: String, content: @Composable (AddressableNote?)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadAddressableNote(aTag: ATag, content: @Composable (AddressableNote?) -> Unit) {
|
||||
fun LoadAddressableNote(aTag: ATag, accountViewModel: AccountViewModel, content: @Composable (AddressableNote?) -> Unit) {
|
||||
var note by remember(aTag) {
|
||||
mutableStateOf<AddressableNote?>(LocalCache.getAddressableNoteIfExists(aTag.toTag()))
|
||||
mutableStateOf<AddressableNote?>(accountViewModel.getAddressableNoteIfExists(aTag.toTag()))
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
LaunchedEffect(key1 = aTag) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newNote = LocalCache.getOrCreateAddressableNote(aTag)
|
||||
accountViewModel.getOrCreateAddressableNote(aTag) { newNote ->
|
||||
if (newNote != note) {
|
||||
note = newNote
|
||||
}
|
||||
@@ -2212,7 +2205,8 @@ private fun EmojiListOptions(
|
||||
accountViewModel.userProfile().pubkeyHex,
|
||||
"",
|
||||
null
|
||||
)
|
||||
),
|
||||
accountViewModel
|
||||
) {
|
||||
it?.let { usersEmojiList ->
|
||||
val hasAddedThis by usersEmojiList.live().metadata.map {
|
||||
@@ -2315,7 +2309,7 @@ fun RenderPinListEvent(
|
||||
TranslatableRichTextViewer(
|
||||
content = pin,
|
||||
canPreview = true,
|
||||
tags = remember { ImmutableListOfLists() },
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -2414,8 +2408,8 @@ private fun RenderReport(
|
||||
TranslatableRichTextViewer(
|
||||
content = content,
|
||||
canPreview = true,
|
||||
modifier = remember { Modifier },
|
||||
tags = remember { ImmutableListOfLists() },
|
||||
modifier = Modifier,
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -2564,6 +2558,7 @@ fun SecondUserInfoRow(
|
||||
@Composable
|
||||
fun LoadStatuses(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (ImmutableList<AddressableNote>) -> Unit
|
||||
) {
|
||||
var statuses: ImmutableList<AddressableNote> by remember {
|
||||
@@ -2573,9 +2568,7 @@ fun LoadStatuses(
|
||||
val userStatus by user.live().statuses.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = userStatus) {
|
||||
launch(Dispatchers.IO) {
|
||||
val myUser = userStatus?.user ?: return@launch
|
||||
val newStatuses = LocalCache.findStatusesForUser(myUser)
|
||||
accountViewModel.findStatusesForUser(userStatus?.user ?: user) { newStatuses ->
|
||||
if (!equalImmutableLists(statuses, newStatuses)) {
|
||||
statuses = newStatuses
|
||||
}
|
||||
@@ -2586,7 +2579,7 @@ fun LoadStatuses(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
|
||||
fun LoadCityName(geohash: String, content: @Composable (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
var cityName by remember(geohash) {
|
||||
mutableStateOf<String>(geohash)
|
||||
@@ -2601,18 +2594,25 @@ fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(cityName),
|
||||
onClick = { nav("Geohash/$geohash") },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colors.primary.copy(
|
||||
alpha = 0.52f
|
||||
content(cityName)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
|
||||
LoadCityName(geohash) { cityName ->
|
||||
ClickableText(
|
||||
text = AnnotatedString(cityName),
|
||||
onClick = { nav("Geohash/$geohash") },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colors.primary.copy(
|
||||
alpha = 0.52f
|
||||
),
|
||||
fontSize = Font14SP,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
fontSize = Font14SP,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
maxLines = 1
|
||||
)
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -2635,12 +2635,14 @@ fun FirstUserInfoRow(
|
||||
}
|
||||
}
|
||||
|
||||
val textColor = if (isRepost) MaterialTheme.colors.grayText else Color.Unspecified
|
||||
|
||||
if (showAuthorPicture) {
|
||||
NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp)
|
||||
Spacer(HalfPadding)
|
||||
NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) })
|
||||
NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) }, textColor = textColor)
|
||||
} else {
|
||||
NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) })
|
||||
NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) }, textColor = textColor)
|
||||
}
|
||||
|
||||
if (isRepost) {
|
||||
@@ -2752,12 +2754,18 @@ private fun RenderAuthorImages(
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
NoteAuthorPicture(baseNote, nav, accountViewModel, Size55dp)
|
||||
val baseRepost by remember {
|
||||
derivedStateOf {
|
||||
baseNote.replyTo?.lastOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
val isRepost = baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent
|
||||
|
||||
if (isRepost) {
|
||||
RepostNoteAuthorPicture(baseNote, accountViewModel, nav)
|
||||
if (isRepost && baseRepost != null) {
|
||||
RepostNoteAuthorPicture(baseNote, baseRepost!!, accountViewModel, nav)
|
||||
} else {
|
||||
NoteAuthorPicture(baseNote, nav, accountViewModel, Size55dp)
|
||||
}
|
||||
|
||||
val isChannel = baseNote.event is ChannelMessageEvent && baseNote.channelHex() != null
|
||||
@@ -2765,7 +2773,7 @@ private fun RenderAuthorImages(
|
||||
if (isChannel) {
|
||||
val baseChannelHex = remember { baseNote.channelHex() }
|
||||
if (baseChannelHex != null) {
|
||||
LoadChannel(baseChannelHex) { channel ->
|
||||
LoadChannel(baseChannelHex, accountViewModel) { channel ->
|
||||
ChannelNotePicture(channel)
|
||||
}
|
||||
}
|
||||
@@ -2773,15 +2781,14 @@ private fun RenderAuthorImages(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadChannel(baseChannelHex: String, content: @Composable (Channel) -> Unit) {
|
||||
fun LoadChannel(baseChannelHex: String, accountViewModel: AccountViewModel, content: @Composable (Channel) -> Unit) {
|
||||
var channel by remember(baseChannelHex) {
|
||||
mutableStateOf<Channel?>(LocalCache.getChannelIfExists(baseChannelHex))
|
||||
mutableStateOf<Channel?>(accountViewModel.getChannelIfExists(baseChannelHex))
|
||||
}
|
||||
|
||||
if (channel == null) {
|
||||
LaunchedEffect(key1 = baseChannelHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newChannel = LocalCache.checkGetOrCreateChannel(baseChannelHex)
|
||||
accountViewModel.checkGetOrCreateChannel(baseChannelHex) { newChannel ->
|
||||
launch(Dispatchers.Main) {
|
||||
channel = newChannel
|
||||
}
|
||||
@@ -2828,23 +2835,30 @@ private fun ChannelNotePicture(baseChannel: Channel) {
|
||||
@Composable
|
||||
private fun RepostNoteAuthorPicture(
|
||||
baseNote: Note,
|
||||
baseRepost: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val baseRepost by remember {
|
||||
derivedStateOf {
|
||||
baseNote.replyTo?.lastOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
baseRepost?.let {
|
||||
Box(Size30Modifier) {
|
||||
Box(modifier = Size55Modifier) {
|
||||
Box(Size35Modifier.align(Alignment.TopStart)) {
|
||||
NoteAuthorPicture(
|
||||
baseNote = it,
|
||||
baseNote = baseNote,
|
||||
nav = nav,
|
||||
accountViewModel = accountViewModel,
|
||||
size = Size30dp,
|
||||
pictureModifier = MaterialTheme.colors.repostProfileBorder
|
||||
size = Size34dp
|
||||
)
|
||||
}
|
||||
|
||||
Box(Size18Modifier.align(Alignment.BottomStart).padding(1.dp)) {
|
||||
RepostedIcon(modifier = Size18Modifier, MaterialTheme.colors.placeholderText)
|
||||
}
|
||||
|
||||
Box(Size35Modifier.align(Alignment.BottomEnd)) {
|
||||
NoteAuthorPicture(
|
||||
baseNote = baseRepost,
|
||||
nav = nav,
|
||||
accountViewModel = accountViewModel,
|
||||
size = Size34dp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2874,7 +2888,7 @@ fun DisplayHighlight(
|
||||
quote,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
remember { Modifier.fillMaxWidth() },
|
||||
remember { ImmutableListOfLists<String>(emptyList()) },
|
||||
EmptyTagList,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav
|
||||
@@ -2891,15 +2905,12 @@ private fun DisplayQuoteAuthor(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var userBase by remember { mutableStateOf<User?>(LocalCache.getUserIfExists(authorHex)) }
|
||||
var userBase by remember { mutableStateOf<User?>(accountViewModel.getUserIfExists(authorHex)) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (userBase == null) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newUserBase = LocalCache.checkGetOrCreateUser(authorHex)
|
||||
launch(Dispatchers.Main) {
|
||||
userBase = newUserBase
|
||||
}
|
||||
if (userBase == null) {
|
||||
LaunchedEffect(Unit) {
|
||||
accountViewModel.checkGetOrCreateUser(authorHex) { newUserBase ->
|
||||
userBase = newUserBase
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2923,7 +2934,7 @@ private fun DisplayQuoteAuthor(
|
||||
|
||||
@Composable
|
||||
private fun LoadAndDisplayPost(postAddress: ATag, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LoadAddressableNote(aTag = postAddress) {
|
||||
LoadAddressableNote(aTag = postAddress, accountViewModel) {
|
||||
it?.let { note ->
|
||||
val noteEvent by note.live().metadata.map {
|
||||
it.note.event
|
||||
@@ -3332,7 +3343,7 @@ fun FileHeaderDisplay(note: Note, roundedCorner: Boolean, accountViewModel: Acco
|
||||
val blurHash = event.blurhash()
|
||||
val hash = event.hash()
|
||||
val dimensions = event.dimensions()
|
||||
val description = event.content
|
||||
val description = event.alt() ?: event.content
|
||||
val isImage = imageExtensions.any { fullUrl.split("?")[0].lowercase().endsWith(it) }
|
||||
val uri = note.toNostrUri()
|
||||
|
||||
@@ -3369,7 +3380,7 @@ fun FileStorageHeaderDisplay(baseNote: Note, roundedCorner: Boolean, accountView
|
||||
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
|
||||
val dataEventId = eventHeader.dataEventId() ?: return
|
||||
|
||||
LoadNote(baseNoteHex = dataEventId) { contentNote ->
|
||||
LoadNote(baseNoteHex = dataEventId, accountViewModel) { contentNote ->
|
||||
if (contentNote != null) {
|
||||
ObserverAndRenderNIP95(baseNote, contentNote, roundedCorner, accountViewModel)
|
||||
}
|
||||
@@ -3396,7 +3407,7 @@ private fun ObserverAndRenderNIP95(
|
||||
val localDir = note?.idHex?.let { File(File(appContext.cacheDir, "NIP95"), it) }
|
||||
val blurHash = eventHeader.blurhash()
|
||||
val dimensions = eventHeader.dimensions()
|
||||
val description = eventHeader.content
|
||||
val description = eventHeader.alt() ?: eventHeader.content
|
||||
val mimeType = eventHeader.mimeType()
|
||||
|
||||
val newContent = if (mimeType?.startsWith("image") == true) {
|
||||
@@ -3444,8 +3455,8 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
|
||||
var participantUsers by remember { mutableStateOf<List<Pair<Participant, User>>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(key1 = participants) {
|
||||
launch(Dispatchers.IO) {
|
||||
participantUsers = participants.mapNotNull { part -> LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) } }
|
||||
accountViewModel.loadParticipants(participants) {
|
||||
participantUsers = it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3524,7 +3535,7 @@ fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: Accou
|
||||
|
||||
val defaultBackground = MaterialTheme.colors.background
|
||||
val background = remember { mutableStateOf(defaultBackground) }
|
||||
val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
||||
val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
val eventContent = remember(note.event) {
|
||||
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
||||
@@ -3654,11 +3665,7 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = eventUpdates) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newParticipantUsers = participants.mapNotNull { part ->
|
||||
LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) }
|
||||
}.toImmutableList()
|
||||
|
||||
accountViewModel.loadParticipants(participants) { newParticipantUsers ->
|
||||
if (!equalImmutableLists(newParticipantUsers, participantUsers)) {
|
||||
participantUsers = newParticipantUsers
|
||||
}
|
||||
@@ -3739,7 +3746,7 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
|
||||
private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountViewModel: AccountViewModel) {
|
||||
val image = remember(noteEvent) { noteEvent.image() }
|
||||
val title = remember(noteEvent) { noteEvent.title() }
|
||||
val summary = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } }
|
||||
val summary = remember(noteEvent) { noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null } }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
@@ -116,7 +117,7 @@ private fun OptionNote(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val tags = remember(baseNote) {
|
||||
baseNote.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists()
|
||||
baseNote.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList
|
||||
}
|
||||
|
||||
Row(
|
||||
|
||||
@@ -83,14 +83,12 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlType
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.TextType
|
||||
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DarkerGreen
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton
|
||||
@@ -136,13 +134,14 @@ fun ReactionsRow(
|
||||
|
||||
InnerReactionRow(baseNote, showReactionDetail, wantsToSeeReactions, accountViewModel, nav)
|
||||
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
|
||||
LoadAndDisplayZapraiser(baseNote, showReactionDetail, wantsToSeeReactions, accountViewModel)
|
||||
|
||||
if (showReactionDetail && wantsToSeeReactions.value) {
|
||||
ReactionDetailGallery(baseNote, nav, accountViewModel)
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
}
|
||||
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -252,7 +251,6 @@ private fun LoadAndDisplayZapraiser(
|
||||
}
|
||||
|
||||
if (zapraiserAmount > 0) {
|
||||
Spacer(modifier = Height4dpModifier)
|
||||
Box(
|
||||
modifier = remember {
|
||||
ReactionRowZapraiserSize
|
||||
@@ -460,8 +458,15 @@ private fun WatchZapAndRenderGallery(
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
val zapEvents by remember(zapsState) {
|
||||
derivedStateOf { baseNote.zaps.mapNotNull { it.value?.let { zapEvent -> CombinedZap(it.key, zapEvent) } }.toImmutableList() }
|
||||
|
||||
var zapEvents by remember(zapsState) {
|
||||
mutableStateOf<ImmutableList<ZapAmountCommentNotification>>(persistentListOf())
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
accountViewModel.decryptAmountMessageInGroup(baseNote) {
|
||||
zapEvents = it
|
||||
}
|
||||
}
|
||||
|
||||
if (zapEvents.isNotEmpty()) {
|
||||
@@ -1361,18 +1366,14 @@ private fun ActionableReactionButton(
|
||||
onChangeAmount: () -> Unit,
|
||||
toRemove: Set<String>
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType
|
||||
)
|
||||
onDismiss()
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
@@ -1383,13 +1384,11 @@ private fun ActionableReactionButton(
|
||||
val thisModifier = remember(reactionType) {
|
||||
Modifier.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType
|
||||
)
|
||||
onDismiss()
|
||||
},
|
||||
onLongClick = {
|
||||
onChangeAmount()
|
||||
|
||||
@@ -314,7 +314,8 @@ private fun EmojiSelector(accountViewModel: AccountViewModel, nav: (String) -> U
|
||||
accountViewModel.userProfile().pubkeyHex,
|
||||
"",
|
||||
null
|
||||
)
|
||||
),
|
||||
accountViewModel
|
||||
) { emptyNote ->
|
||||
emptyNote?.let { usersEmojiList ->
|
||||
val collections by usersEmojiList.live().metadata.map {
|
||||
@@ -339,7 +340,7 @@ fun EmojiCollectionGallery(emojiCollections: List<ATag>, accountViewModel: Accou
|
||||
state = listState
|
||||
) {
|
||||
itemsIndexed(emojiCollections, key = { _, item -> item.toTag() }) { _, item ->
|
||||
LoadAddressableNote(aTag = item) {
|
||||
LoadAddressableNote(aTag = item, accountViewModel) {
|
||||
it?.let {
|
||||
WatchAndRenderNote(it, bgColor, accountViewModel, nav, onClick)
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
@@ -219,8 +220,7 @@ fun UpdateZapAmountDialog(
|
||||
Triple(LnZapEvent.ZapType.NONZAP, stringResource(id = R.string.zap_type_nonzap), stringResource(id = R.string.zap_type_nonzap_explainer))
|
||||
)
|
||||
|
||||
val zapOptions = remember { zapTypes.map { it.second }.toImmutableList() }
|
||||
val zapOptionExplainers = remember { zapTypes.map { it.third }.toImmutableList() }
|
||||
val zapOptions = remember { zapTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
|
||||
|
||||
LaunchedEffect(accountViewModel, nip47uri) {
|
||||
postViewModel.load()
|
||||
@@ -361,7 +361,6 @@ fun UpdateZapAmountDialog(
|
||||
placeholder = zapTypes.filter { it.first == accountViewModel.defaultZapType() }
|
||||
.first().second,
|
||||
options = zapOptions,
|
||||
explainers = zapOptionExplainers,
|
||||
onSelect = {
|
||||
postViewModel.selectedZapType = zapTypes[it].first
|
||||
},
|
||||
|
||||
@@ -112,7 +112,7 @@ fun UserPicture(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadUser(baseUserHex = userHex) {
|
||||
LoadUser(baseUserHex = userHex, accountViewModel) {
|
||||
if (it != null) {
|
||||
UserPicture(
|
||||
user = it,
|
||||
@@ -216,57 +216,57 @@ fun NonClickableUserPictures(
|
||||
|
||||
when (userList.size) {
|
||||
0 -> {}
|
||||
1 -> LoadUser(baseUserHex = userList[0]) {
|
||||
1 -> LoadUser(baseUserHex = userList[0], accountViewModel) {
|
||||
it?.let {
|
||||
BaseUserPicture(it, size, accountViewModel, outerModifier = Modifier)
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
LoadUser(baseUserHex = userList[0]) {
|
||||
LoadUser(baseUserHex = userList[0], accountViewModel) {
|
||||
it?.let {
|
||||
BaseUserPicture(it, size.div(1.5f), accountViewModel, outerModifier = Modifier.align(Alignment.CenterStart))
|
||||
}
|
||||
}
|
||||
LoadUser(baseUserHex = userList[1]) {
|
||||
LoadUser(baseUserHex = userList[1], accountViewModel) {
|
||||
it?.let {
|
||||
BaseUserPicture(it, size.div(1.5f), accountViewModel, outerModifier = Modifier.align(Alignment.CenterEnd))
|
||||
}
|
||||
}
|
||||
}
|
||||
3 -> {
|
||||
LoadUser(baseUserHex = userList[0]) {
|
||||
LoadUser(baseUserHex = userList[0], accountViewModel) {
|
||||
it?.let {
|
||||
BaseUserPicture(it, size.div(1.8f), accountViewModel, outerModifier = Modifier.align(Alignment.BottomStart))
|
||||
}
|
||||
}
|
||||
LoadUser(baseUserHex = userList[1]) {
|
||||
LoadUser(baseUserHex = userList[1], accountViewModel) {
|
||||
it?.let {
|
||||
BaseUserPicture(it, size.div(1.8f), accountViewModel, outerModifier = Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
LoadUser(baseUserHex = userList[2]) {
|
||||
LoadUser(baseUserHex = userList[2], accountViewModel) {
|
||||
it?.let {
|
||||
BaseUserPicture(it, size.div(1.8f), accountViewModel, outerModifier = Modifier.align(Alignment.BottomEnd))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
LoadUser(baseUserHex = userList[0]) {
|
||||
LoadUser(baseUserHex = userList[0], accountViewModel) {
|
||||
it?.let {
|
||||
BaseUserPicture(it, size.div(2f), accountViewModel, outerModifier = Modifier.align(Alignment.BottomStart))
|
||||
}
|
||||
}
|
||||
LoadUser(baseUserHex = userList[1]) {
|
||||
LoadUser(baseUserHex = userList[1], accountViewModel) {
|
||||
it?.let {
|
||||
BaseUserPicture(it, size.div(2f), accountViewModel, outerModifier = Modifier.align(Alignment.TopStart))
|
||||
}
|
||||
}
|
||||
LoadUser(baseUserHex = userList[2]) {
|
||||
LoadUser(baseUserHex = userList[2], accountViewModel) {
|
||||
it?.let {
|
||||
BaseUserPicture(it, size.div(2f), accountViewModel, outerModifier = Modifier.align(Alignment.BottomEnd))
|
||||
}
|
||||
}
|
||||
LoadUser(baseUserHex = userList[3]) {
|
||||
LoadUser(baseUserHex = userList[3], accountViewModel) {
|
||||
it?.let {
|
||||
BaseUserPicture(it, size.div(2f), accountViewModel, outerModifier = Modifier.align(Alignment.TopEnd))
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -31,20 +32,20 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
|
||||
@Composable
|
||||
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier, showPlayButton: Boolean = true) {
|
||||
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier, showPlayButton: Boolean = true, textColor: Color = Color.Unspecified) {
|
||||
val authorState by baseNote.live().metadata.map {
|
||||
it.note.author
|
||||
}.observeAsState(baseNote.author)
|
||||
|
||||
Crossfade(targetState = authorState, modifier = weight) {
|
||||
it?.let {
|
||||
UsernameDisplay(it, weight, showPlayButton)
|
||||
UsernameDisplay(it, weight, showPlayButton, textColor = textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier, showPlayButton: Boolean = true, fontWeight: FontWeight = FontWeight.Bold) {
|
||||
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier, showPlayButton: Boolean = true, fontWeight: FontWeight = FontWeight.Bold, textColor: Color = Color.Unspecified) {
|
||||
val npubDisplay by remember {
|
||||
derivedStateOf {
|
||||
baseUser.pubkeyDisplayHex()
|
||||
@@ -57,9 +58,9 @@ fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier, showPlayButton:
|
||||
|
||||
Crossfade(targetState = userMetadata, modifier = weight) {
|
||||
if (it != null) {
|
||||
UserNameDisplay(it.bestUsername(), it.bestDisplayName(), npubDisplay, it.tags, weight, showPlayButton, fontWeight)
|
||||
UserNameDisplay(it.bestUsername(), it.bestDisplayName(), npubDisplay, it.tags, weight, showPlayButton, fontWeight, textColor)
|
||||
} else {
|
||||
NPubDisplay(npubDisplay, weight, fontWeight)
|
||||
NPubDisplay(npubDisplay, weight, fontWeight, textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,27 +73,29 @@ private fun UserNameDisplay(
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
modifier: Modifier,
|
||||
showPlayButton: Boolean = true,
|
||||
fontWeight: FontWeight = FontWeight.Bold
|
||||
fontWeight: FontWeight = FontWeight.Bold,
|
||||
textColor: Color = Color.Unspecified
|
||||
) {
|
||||
if (bestUserName != null && bestDisplayName != null && bestDisplayName != bestUserName) {
|
||||
UserAndUsernameDisplay(bestDisplayName.trim(), tags, bestUserName.trim(), modifier, showPlayButton, fontWeight)
|
||||
UserAndUsernameDisplay(bestDisplayName.trim(), tags, bestUserName.trim(), modifier, showPlayButton, fontWeight, textColor)
|
||||
} else if (bestDisplayName != null) {
|
||||
UserDisplay(bestDisplayName.trim(), tags, modifier, showPlayButton, fontWeight)
|
||||
UserDisplay(bestDisplayName.trim(), tags, modifier, showPlayButton, fontWeight, textColor)
|
||||
} else if (bestUserName != null) {
|
||||
UserDisplay(bestUserName.trim(), tags, modifier, showPlayButton, fontWeight)
|
||||
UserDisplay(bestUserName.trim(), tags, modifier, showPlayButton, fontWeight, textColor)
|
||||
} else {
|
||||
NPubDisplay(npubDisplay, modifier, fontWeight)
|
||||
NPubDisplay(npubDisplay, modifier, fontWeight, textColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NPubDisplay(npubDisplay: String, modifier: Modifier, fontWeight: FontWeight = FontWeight.Bold) {
|
||||
fun NPubDisplay(npubDisplay: String, modifier: Modifier, fontWeight: FontWeight = FontWeight.Bold, textColor: Color = Color.Unspecified) {
|
||||
Text(
|
||||
text = npubDisplay,
|
||||
fontWeight = fontWeight,
|
||||
modifier = modifier,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
|
||||
@@ -102,7 +105,8 @@ private fun UserDisplay(
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
modifier: Modifier,
|
||||
showPlayButton: Boolean = true,
|
||||
fontWeight: FontWeight = FontWeight.Bold
|
||||
fontWeight: FontWeight = FontWeight.Bold,
|
||||
textColor: Color = Color.Unspecified
|
||||
) {
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
CreateTextWithEmoji(
|
||||
@@ -111,7 +115,8 @@ private fun UserDisplay(
|
||||
fontWeight = fontWeight,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
color = textColor
|
||||
)
|
||||
if (showPlayButton) {
|
||||
Spacer(StdHorzSpacer)
|
||||
@@ -127,7 +132,8 @@ private fun UserAndUsernameDisplay(
|
||||
bestUserName: String,
|
||||
modifier: Modifier,
|
||||
showPlayButton: Boolean = true,
|
||||
fontWeight: FontWeight = FontWeight.Bold
|
||||
fontWeight: FontWeight = FontWeight.Bold,
|
||||
textColor: Color = Color.Unspecified
|
||||
) {
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
CreateTextWithEmoji(
|
||||
@@ -135,7 +141,8 @@ private fun UserAndUsernameDisplay(
|
||||
tags = tags,
|
||||
fontWeight = fontWeight,
|
||||
maxLines = 1,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
color = textColor
|
||||
)
|
||||
/*
|
||||
CreateTextWithEmoji(
|
||||
|
||||
@@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
@@ -107,8 +108,7 @@ fun ZapCustomDialog(
|
||||
Triple(LnZapEvent.ZapType.NONZAP, stringResource(id = R.string.zap_type_nonzap), stringResource(id = R.string.zap_type_nonzap_explainer))
|
||||
)
|
||||
|
||||
val zapOptions = remember { zapTypes.map { it.second }.toImmutableList() }
|
||||
val zapOptionExplainers = remember { zapTypes.map { it.third }.toImmutableList() }
|
||||
val zapOptions = remember { zapTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
|
||||
var selectedZapType by remember(accountViewModel) { mutableStateOf(accountViewModel.account.defaultZapType) }
|
||||
|
||||
Dialog(
|
||||
@@ -181,7 +181,6 @@ fun ZapCustomDialog(
|
||||
label = stringResource(id = R.string.zap_type),
|
||||
placeholder = zapTypes.filter { it.first == accountViewModel.account.defaultZapType }.first().second,
|
||||
options = zapOptions,
|
||||
explainers = zapOptionExplainers,
|
||||
onSelect = {
|
||||
selectedZapType = zapTypes[it].first
|
||||
},
|
||||
|
||||
@@ -26,7 +26,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.ZapReqResponse
|
||||
@@ -40,7 +39,6 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -53,16 +51,9 @@ fun ZapNoteCompose(baseReqResponse: ZapReqResponse, accountViewModel: AccountVie
|
||||
}
|
||||
|
||||
LaunchedEffect(baseNoteRequest) {
|
||||
launch(Dispatchers.Default) {
|
||||
(baseNoteRequest?.note?.event as? LnZapRequestEvent)?.let {
|
||||
baseNoteRequest?.note?.let {
|
||||
val decryptedContent = accountViewModel.decryptZap(it)
|
||||
if (decryptedContent != null) {
|
||||
baseAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
} else {
|
||||
baseAuthor = it.author
|
||||
}
|
||||
}
|
||||
baseNoteRequest?.note?.let {
|
||||
accountViewModel.decryptAmountMessage(it, baseReqResponse.zapEvent) {
|
||||
baseAuthor = it?.user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -24,8 +25,10 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -84,11 +87,33 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
|
||||
val zapEvents by remember { derivedStateOf { zapSetCard.zapEvents } }
|
||||
AuthorGalleryZaps(zapEvents, backgroundColor, nav, accountViewModel)
|
||||
Column(modifier = Modifier) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
MapZaps(zapSetCard.zapEvents, accountViewModel) {
|
||||
AuthorGalleryZaps(it, backgroundColor, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
UserCompose(baseUser = zapSetCard.user, accountViewModel = accountViewModel, nav = nav)
|
||||
Spacer(DoubleVertSpacer)
|
||||
|
||||
Row(Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp).fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
UserPicture(
|
||||
zapSetCard.user,
|
||||
Size55dp,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
Column(modifier = remember { Modifier.padding(start = 10.dp).weight(1f) }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
UsernameDisplay(zapSetCard.user)
|
||||
}
|
||||
|
||||
AboutDisplay(zapSetCard.user)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(DoubleVertSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Stable
|
||||
@@ -173,6 +176,8 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
|
||||
|
||||
val allBaseNotes = zapsPerEvent.keys + boostsPerEvent.keys + reactionsPerEvent.keys
|
||||
val multiCards = allBaseNotes.map { baseNote ->
|
||||
val boostsInCard = boostsPerEvent[baseNote] ?: emptyList()
|
||||
@@ -180,26 +185,47 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
val zapsInCard = zapsPerEvent[baseNote] ?: emptyList()
|
||||
|
||||
val singleList = (boostsInCard + zapsInCard.map { it.response } + reactionsInCard)
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
.groupBy {
|
||||
sdf.format(
|
||||
Instant.ofEpochSecond(it.createdAt() ?: 0)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime()
|
||||
)
|
||||
}
|
||||
|
||||
singleList.chunked(30).map { chunk ->
|
||||
MultiSetCard(
|
||||
baseNote,
|
||||
boostsInCard.filter { it in chunk }.toImmutableList(),
|
||||
reactionsInCard.filter { it in chunk }.toImmutableList(),
|
||||
zapsInCard.filter { it.response in chunk }.toImmutableList()
|
||||
val days = singleList.keys.sortedBy { it }
|
||||
|
||||
days.mapNotNull {
|
||||
val sortedList = singleList.get(it)?.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))?.reversed()
|
||||
|
||||
sortedList?.chunked(30)?.map { chunk ->
|
||||
MultiSetCard(
|
||||
baseNote,
|
||||
boostsInCard.filter { it in chunk }.toImmutableList(),
|
||||
reactionsInCard.filter { it in chunk }.toImmutableList(),
|
||||
zapsInCard.filter { it.response in chunk }.toImmutableList()
|
||||
)
|
||||
}
|
||||
}.flatten()
|
||||
}.flatten()
|
||||
|
||||
val userZaps = zapsPerUser.map { user ->
|
||||
val byDay = user.value.groupBy {
|
||||
sdf.format(
|
||||
Instant.ofEpochSecond(it.createdAt() ?: 0)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime()
|
||||
)
|
||||
}
|
||||
|
||||
byDay.values.map {
|
||||
ZapUserSetCard(
|
||||
user.key,
|
||||
it.sortedWith(compareBy({ it.createdAt() }, { it.idHex() })).reversed().toImmutableList()
|
||||
)
|
||||
}
|
||||
}.flatten()
|
||||
|
||||
val userZaps = zapsPerUser.map {
|
||||
ZapUserSetCard(
|
||||
it.key,
|
||||
it.value.toImmutableList()
|
||||
)
|
||||
}
|
||||
|
||||
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is GenericRepostEvent && it.event !is LnZapEvent }.map {
|
||||
if (it.event is PrivateDmEvent || it.event is ChatMessageEvent) {
|
||||
MessageSetCard(it)
|
||||
|
||||
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.note.*
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -146,7 +147,7 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { index, item ->
|
||||
if (index == 0) {
|
||||
ProvideTextStyle(TextStyle(fontSize = 16.sp, lineHeight = 1.20.em)) {
|
||||
ProvideTextStyle(TextStyle(fontSize = 18.sp, lineHeight = 1.20.em)) {
|
||||
NoteMaster(
|
||||
item,
|
||||
modifier = Modifier.drawReplyLevel(
|
||||
@@ -463,6 +464,13 @@ fun NoteMaster(
|
||||
}
|
||||
}
|
||||
|
||||
val noteEvent = baseNote.event
|
||||
val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false }
|
||||
if (zapSplits && noteEvent != null) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
DisplayZapSplits(noteEvent, accountViewModel, nav)
|
||||
}
|
||||
|
||||
ReactionsRow(note, true, accountViewModel, nav)
|
||||
|
||||
Divider(
|
||||
@@ -589,20 +597,20 @@ private fun RenderLongFormHeaderForThread(noteEvent: LongTextNoteEvent) {
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
|
||||
noteEvent.title()?.let {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
|
||||
noteEvent.summary()?.let {
|
||||
noteEvent.summary()?.ifBlank { null }?.let {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
@@ -101,10 +101,12 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), In
|
||||
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||
|
||||
override fun invalidateData(ignoreIfDoing: Boolean) {
|
||||
bundler.invalidate(ignoreIfDoing) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
refreshSuspended()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
bundler.invalidate(ignoreIfDoing) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+161
-7
@@ -12,6 +12,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -24,15 +25,20 @@ import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.actions.Dao
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.events.Participant
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.events.UserMetadata
|
||||
@@ -40,6 +46,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -47,7 +54,7 @@ import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
@Stable
|
||||
class AccountViewModel(val account: Account) : ViewModel() {
|
||||
class AccountViewModel(val account: Account) : ViewModel(), Dao {
|
||||
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
||||
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
|
||||
val accountLastReadLiveData: LiveData<AccountState> = account.liveLastRead.map { it }
|
||||
@@ -110,11 +117,13 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
}
|
||||
|
||||
fun reactToOrDelete(note: Note, reaction: String) {
|
||||
val currentReactions = account.reactionTo(note, reaction)
|
||||
if (currentReactions.isNotEmpty()) {
|
||||
account.delete(currentReactions)
|
||||
} else {
|
||||
account.reactTo(note, reaction)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val currentReactions = account.reactionTo(note, reaction)
|
||||
if (currentReactions.isNotEmpty()) {
|
||||
account.delete(currentReactions)
|
||||
} else {
|
||||
account.reactTo(note, reaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +184,38 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptAmountMessageInGroup(
|
||||
zaps: ImmutableList<CombinedZap>,
|
||||
onNewState: (ImmutableList<ZapAmountCommentNotification>) -> Unit
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val list = ArrayList<ZapAmountCommentNotification>(zaps.size)
|
||||
zaps.forEach {
|
||||
innerDecryptAmountMessage(it.request, it.response)?.let {
|
||||
list.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
onNewState(list.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptAmountMessageInGroup(
|
||||
baseNote: Note,
|
||||
onNewState: (ImmutableList<ZapAmountCommentNotification>) -> Unit
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val list = ArrayList<ZapAmountCommentNotification>(baseNote.zaps.size)
|
||||
baseNote.zaps.forEach {
|
||||
innerDecryptAmountMessage(it.key, it.value)?.let {
|
||||
list.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
onNewState(list.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptAmountMessage(
|
||||
zapRequest: Note,
|
||||
zapEvent: Note?,
|
||||
@@ -189,6 +230,8 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
zapRequest: Note,
|
||||
zapEvent: Note?
|
||||
): ZapAmountCommentNotification? {
|
||||
checkNotInMainThread()
|
||||
|
||||
(zapRequest.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = decryptZap(zapRequest)
|
||||
val amount = (zapEvent?.event as? LnZapEvent)?.amount
|
||||
@@ -386,7 +429,7 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
)
|
||||
|
||||
fun isNoteAcceptable(note: Note, onReady: (NoteComposeReportState) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
|
||||
val isFromLoggedInFollow = note.author?.let { userProfile().isFollowingCached(it) } ?: true
|
||||
|
||||
@@ -523,6 +566,117 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun runOnIO(runOnIO: () -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
runOnIO()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkGetOrCreateUser(key: HexKey): User? {
|
||||
return LocalCache.checkGetOrCreateUser(key)
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateUser(key: HexKey): User {
|
||||
return LocalCache.getOrCreateUser(key)
|
||||
}
|
||||
|
||||
fun checkGetOrCreateUser(key: HexKey, onResult: (User?) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onResult(checkGetOrCreateUser(key))
|
||||
}
|
||||
}
|
||||
|
||||
fun getUserIfExists(hex: HexKey): User? {
|
||||
return LocalCache.getUserIfExists(hex)
|
||||
}
|
||||
|
||||
suspend fun checkGetOrCreateNote(key: HexKey): Note? {
|
||||
return LocalCache.checkGetOrCreateNote(key)
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateNote(key: HexKey): Note {
|
||||
return LocalCache.getOrCreateNote(key)
|
||||
}
|
||||
|
||||
fun checkGetOrCreateNote(key: HexKey, onResult: (Note?) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onResult(checkGetOrCreateNote(key))
|
||||
}
|
||||
}
|
||||
|
||||
fun getNoteIfExists(hex: HexKey): Note? {
|
||||
return LocalCache.getNoteIfExists(hex)
|
||||
}
|
||||
|
||||
override suspend fun checkGetOrCreateAddressableNote(key: HexKey): AddressableNote? {
|
||||
return LocalCache.checkGetOrCreateAddressableNote(key)
|
||||
}
|
||||
|
||||
fun checkGetOrCreateAddressableNote(key: HexKey, onResult: (AddressableNote?) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onResult(checkGetOrCreateAddressableNote(key))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? {
|
||||
return LocalCache.getOrCreateAddressableNote(key)
|
||||
}
|
||||
|
||||
fun getOrCreateAddressableNote(key: ATag, onResult: (AddressableNote?) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onResult(getOrCreateAddressableNote(key))
|
||||
}
|
||||
}
|
||||
|
||||
fun getAddressableNoteIfExists(key: String): AddressableNote? {
|
||||
return LocalCache.addressables[key]
|
||||
}
|
||||
|
||||
fun findStatusesForUser(myUser: User, onResult: (ImmutableList<AddressableNote>) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onResult(LocalCache.findStatusesForUser(myUser))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkGetOrCreateChannel(key: HexKey): Channel? {
|
||||
return LocalCache.checkGetOrCreateChannel(key)
|
||||
}
|
||||
|
||||
fun checkGetOrCreateChannel(key: HexKey, onResult: (Channel?) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onResult(checkGetOrCreateChannel(key))
|
||||
}
|
||||
}
|
||||
|
||||
fun getChannelIfExists(hex: HexKey): Channel? {
|
||||
return LocalCache.getChannelIfExists(hex)
|
||||
}
|
||||
|
||||
fun loadParticipants(participants: List<Participant>, onReady: (ImmutableList<Pair<Participant, User>>) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val participantUsers = participants.mapNotNull { part ->
|
||||
checkGetOrCreateUser(part.key)?.let {
|
||||
Pair(
|
||||
part,
|
||||
it
|
||||
)
|
||||
}
|
||||
}.toImmutableList()
|
||||
|
||||
onReady(participantUsers)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadUsers(hexList: List<String>, onReady: (ImmutableList<User>) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onReady(
|
||||
hexList.mapNotNull { hex ->
|
||||
checkGetOrCreateUser(hex)
|
||||
}.sortedBy { account.isFollowing(it) }.reversed().toImmutableList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
|
||||
return AccountViewModel(account) as AccountViewModel
|
||||
|
||||
@@ -129,7 +129,7 @@ import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
import com.vitorpamplona.quartz.events.Participant
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
@@ -149,7 +149,7 @@ fun ChannelScreen(
|
||||
) {
|
||||
if (channelId == null) return
|
||||
|
||||
LoadChannel(channelId) {
|
||||
LoadChannel(channelId, accountViewModel) {
|
||||
PrepareChannelViewModels(
|
||||
baseChannel = it,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -169,6 +169,7 @@ fun PrepareChannelViewModels(baseChannel: Channel, accountViewModel: AccountView
|
||||
)
|
||||
|
||||
val channelScreenModel: NewPostViewModel = viewModel()
|
||||
channelScreenModel.accountViewModel = accountViewModel
|
||||
channelScreenModel.account = accountViewModel.account
|
||||
|
||||
ChannelScreen(
|
||||
@@ -267,7 +268,8 @@ fun ChannelScreen(
|
||||
message = newPostModel.message.text,
|
||||
mentions = listOfNotNull(replyTo.value?.author),
|
||||
replyTos = listOfNotNull(replyTo.value),
|
||||
channelHex = channel.idHex
|
||||
channelHex = channel.idHex,
|
||||
dao = accountViewModel
|
||||
)
|
||||
tagger.run()
|
||||
if (channel is PublicChatChannel) {
|
||||
@@ -530,7 +532,7 @@ fun ChannelHeader(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadChannel(channelHex) {
|
||||
LoadChannel(channelHex, accountViewModel) {
|
||||
ChannelHeader(
|
||||
it,
|
||||
showVideo,
|
||||
@@ -749,9 +751,9 @@ fun LongChannelHeader(
|
||||
|
||||
val tags = remember(channelState) {
|
||||
if (baseChannel is LiveActivitiesChannel) {
|
||||
baseChannel.info?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists()
|
||||
baseChannel.info?.tags()?.toImmutableListOfLists() ?: EmptyTagList
|
||||
} else {
|
||||
ImmutableListOfLists()
|
||||
EmptyTagList
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,7 +785,7 @@ fun LongChannelHeader(
|
||||
}
|
||||
}
|
||||
|
||||
LoadNote(baseNoteHex = channel.idHex) { loadingNote ->
|
||||
LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote ->
|
||||
loadingNote?.let { note ->
|
||||
Row(
|
||||
lineModifier,
|
||||
@@ -886,7 +888,7 @@ private fun ShortChannelActionOptions(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadNote(baseNoteHex = channel.idHex) {
|
||||
LoadNote(baseNoteHex = channel.idHex, accountViewModel) {
|
||||
it?.let {
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
@@ -185,6 +185,7 @@ fun PrepareChatroomViewModels(
|
||||
)
|
||||
|
||||
val newPostModel: NewPostViewModel = viewModel()
|
||||
newPostModel.accountViewModel = accountViewModel
|
||||
newPostModel.account = accountViewModel.account
|
||||
newPostModel.requiresNIP24 = room.users.size > 1
|
||||
if (newPostModel.requiresNIP24) {
|
||||
@@ -482,7 +483,7 @@ fun ChatroomHeader(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
if (room.users.size == 1) {
|
||||
LoadUser(baseUserHex = room.users.first()) { baseUser ->
|
||||
LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser ->
|
||||
if (baseUser != null) {
|
||||
ChatroomHeader(baseUser = baseUser, modifier = modifier, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
@@ -559,7 +560,7 @@ fun GroupChatroomHeader(
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
RoomNameOnlyDisplay(room, Modifier, FontWeight.Bold, accountViewModel.userProfile())
|
||||
DisplayUserSetAsSubject(room, FontWeight.Normal)
|
||||
DisplayUserSetAsSubject(room, accountViewModel, FontWeight.Normal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,7 +734,7 @@ fun LongRoomHeader(
|
||||
state = rememberLazyListState()
|
||||
) {
|
||||
itemsIndexed(list, key = { _, item -> item }) { _, item ->
|
||||
LoadUser(baseUserHex = item) {
|
||||
LoadUser(baseUserHex = item, accountViewModel) {
|
||||
if (it != null) {
|
||||
UserCompose(
|
||||
baseUser = it,
|
||||
|
||||
@@ -22,7 +22,7 @@ import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
fun CommunityScreen(aTagHex: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
if (aTagHex == null) return
|
||||
|
||||
LoadAddressableNote(aTagHex = aTagHex) {
|
||||
LoadAddressableNote(aTagHex = aTagHex, accountViewModel) {
|
||||
it?.let {
|
||||
PrepareViewModelsCommunityScreen(
|
||||
note = it,
|
||||
|
||||
@@ -2,8 +2,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.animation.with
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -15,11 +21,18 @@ import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
@@ -29,6 +42,7 @@ import com.vitorpamplona.amethyst.ui.buttons.NewCommunityNoteButton
|
||||
import com.vitorpamplona.amethyst.ui.buttons.NewImageButton
|
||||
import com.vitorpamplona.amethyst.ui.buttons.NewNoteButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.*
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route.Companion.InvertedLayouts
|
||||
import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountState
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
@@ -43,8 +57,9 @@ import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -180,6 +195,33 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val bottomBarHeightPx = with(LocalDensity.current) { 50.dp.roundToPx().toFloat() }
|
||||
val bottomBarOffsetHeightPx = remember { mutableStateOf(0f) }
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val delta = available.y
|
||||
val newOffset = bottomBarOffsetHeightPx.value + delta
|
||||
|
||||
val currentRoute = navState.value?.destination?.route
|
||||
|
||||
bottomBarOffsetHeightPx.value = if (currentRoute !in InvertedLayouts) {
|
||||
newOffset.coerceIn(-bottomBarHeightPx, 0f)
|
||||
} else {
|
||||
newOffset.coerceIn(0f, bottomBarHeightPx)
|
||||
}
|
||||
|
||||
return Offset.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
val shouldShow = remember {
|
||||
derivedStateOf {
|
||||
abs(bottomBarOffsetHeightPx.value) < bottomBarHeightPx / 2.0f
|
||||
}
|
||||
}
|
||||
|
||||
ModalBottomSheetLayout(
|
||||
sheetState = sheetState,
|
||||
sheetContent = {
|
||||
@@ -189,12 +231,40 @@ fun MainScreen(
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.primaryVariant)
|
||||
.statusBarsPadding(),
|
||||
.statusBarsPadding()
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
bottomBar = {
|
||||
AppBottomBar(accountViewModel, navState, navBottomRow)
|
||||
AnimatedContent(
|
||||
targetState = shouldShow.value,
|
||||
transitionSpec = {
|
||||
slideInVertically { height -> height } togetherWith
|
||||
slideOutVertically { height -> height }
|
||||
}
|
||||
) { isVisible ->
|
||||
if (isVisible) {
|
||||
AppBottomBar(accountViewModel, navState, navBottomRow)
|
||||
}
|
||||
}
|
||||
},
|
||||
topBar = {
|
||||
AppTopBar(followLists, navState, scaffoldState, accountViewModel, nav = nav, navPopBack)
|
||||
AnimatedContent(
|
||||
targetState = shouldShow.value,
|
||||
transitionSpec = {
|
||||
slideInVertically { height -> 0 } togetherWith
|
||||
slideOutVertically { height -> 0 }
|
||||
}
|
||||
) { isVisible ->
|
||||
if (isVisible) {
|
||||
AppTopBar(
|
||||
followLists,
|
||||
navState,
|
||||
scaffoldState,
|
||||
accountViewModel,
|
||||
nav = nav,
|
||||
navPopBack
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
drawerContent = {
|
||||
DrawerContent(nav, scaffoldState, sheetState, accountViewModel)
|
||||
@@ -203,11 +273,27 @@ fun MainScreen(
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingButtons(navState, accountViewModel, accountStateViewModel, nav, navBottomRow)
|
||||
Crossfade(
|
||||
targetState = shouldShow.value,
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
) { state ->
|
||||
if (state) {
|
||||
FloatingButtons(
|
||||
navState,
|
||||
accountViewModel,
|
||||
accountStateViewModel,
|
||||
nav,
|
||||
navBottomRow
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scaffoldState = scaffoldState
|
||||
) {
|
||||
Column(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(bottom = if (shouldShow.value) it.calculateBottomPadding() else 0.dp)
|
||||
) {
|
||||
AppNavigation(
|
||||
homeFeedViewModel = homeFeedViewModel,
|
||||
repliesFeedViewModel = repliesFeedViewModel,
|
||||
|
||||
@@ -54,6 +54,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -97,9 +98,9 @@ import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.GitHubIdentity
|
||||
import com.vitorpamplona.quartz.events.IdentityClaim
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.MastodonIdentity
|
||||
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.events.PayInvoiceSuccessResponse
|
||||
@@ -118,7 +119,7 @@ import java.math.BigDecimal
|
||||
fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
if (userId == null) return
|
||||
|
||||
var userBase by remember { mutableStateOf<User?>(LocalCache.getUserIfExists(userId)) }
|
||||
var userBase by remember { mutableStateOf(LocalCache.getUserIfExists(userId)) }
|
||||
|
||||
if (userBase == null) {
|
||||
LaunchedEffect(userId) {
|
||||
@@ -280,7 +281,6 @@ fun ProfileScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun RenderSurface(
|
||||
baseUser: User,
|
||||
threadsViewModel: NostrUserProfileNewThreadsFeedViewModel,
|
||||
@@ -383,7 +383,7 @@ private fun RenderScreen(
|
||||
) {
|
||||
val pagerState = rememberPagerState { 9 }
|
||||
|
||||
Column() {
|
||||
Column {
|
||||
ProfileHeader(baseUser, appRecommendations, nav, accountViewModel)
|
||||
ScrollableTabRow(
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
@@ -449,7 +449,7 @@ private fun CreateAndRenderTabs(
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val tabs = listOf<@Composable() (() -> Unit)?>(
|
||||
val tabs = listOf<@Composable (() -> Unit)?>(
|
||||
{ Text(text = stringResource(R.string.notes)) },
|
||||
{ Text(text = stringResource(R.string.replies)) },
|
||||
{ FollowTabHeader(baseUser) },
|
||||
@@ -486,7 +486,7 @@ private fun RelaysTabHeader(baseUser: User) {
|
||||
@Composable
|
||||
private fun ReportsTabHeader(baseUser: User) {
|
||||
val userState by baseUser.live().reports.observeAsState()
|
||||
var userReports by remember { mutableStateOf(0) }
|
||||
var userReports by remember { mutableIntStateOf(0) }
|
||||
|
||||
LaunchedEffect(key1 = userState) {
|
||||
launch(Dispatchers.IO) {
|
||||
@@ -503,11 +503,11 @@ private fun ReportsTabHeader(baseUser: User) {
|
||||
|
||||
@Composable
|
||||
private fun FollowedTagsTabHeader(baseUser: User) {
|
||||
var usertags by remember { mutableStateOf(0) }
|
||||
var usertags by remember { mutableIntStateOf(0) }
|
||||
|
||||
LaunchedEffect(key1 = baseUser) {
|
||||
launch(Dispatchers.IO) {
|
||||
val contactList = baseUser?.latestContactList
|
||||
val contactList = baseUser.latestContactList
|
||||
|
||||
val newTags = (contactList?.verifiedFollowTagSet?.count() ?: 0)
|
||||
|
||||
@@ -524,7 +524,7 @@ private fun FollowedTagsTabHeader(baseUser: User) {
|
||||
private fun BookmarkTabHeader(baseUser: User) {
|
||||
val userState by baseUser.live().bookmarks.observeAsState()
|
||||
|
||||
var userBookmarks by remember { mutableStateOf(0) }
|
||||
var userBookmarks by remember { mutableIntStateOf(0) }
|
||||
|
||||
LaunchedEffect(key1 = userState) {
|
||||
launch(Dispatchers.IO) {
|
||||
@@ -716,8 +716,6 @@ private fun ProfileActions(
|
||||
EditButton(accountViewModel.account)
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
WatchIsHiddenUser(baseUser, accountViewModel) { isHidden ->
|
||||
if (isHidden) {
|
||||
ShowUserButton {
|
||||
@@ -940,7 +938,7 @@ private fun DrawAdditionalInfo(
|
||||
}
|
||||
}
|
||||
|
||||
DisplayBadges(baseUser, nav)
|
||||
DisplayBadges(baseUser, accountViewModel, nav)
|
||||
|
||||
DisplayNip05ProfileStatus(user, accountViewModel)
|
||||
|
||||
@@ -1002,7 +1000,7 @@ private fun DrawAdditionalInfo(
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = false,
|
||||
tags = remember { ImmutableListOfLists<String>(emptyList()) },
|
||||
tags = EmptyTagList,
|
||||
backgroundColor = background,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
@@ -1114,7 +1112,7 @@ private fun DisplayAppRecommendations(
|
||||
) { state ->
|
||||
when (state) {
|
||||
is FeedState.Loaded -> {
|
||||
Column() {
|
||||
Column {
|
||||
Text(stringResource(id = R.string.recommended_apps))
|
||||
|
||||
FlowRow(
|
||||
@@ -1137,7 +1135,7 @@ private fun DisplayAppRecommendations(
|
||||
private fun WatchApp(baseApp: Note, nav: (String) -> Unit) {
|
||||
val appState by baseApp.live().metadata.observeAsState()
|
||||
|
||||
var appLogo by remember { mutableStateOf<String?>(null) }
|
||||
var appLogo by remember(baseApp) { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(key1 = appState) {
|
||||
launch(Dispatchers.Default) {
|
||||
@@ -1174,6 +1172,7 @@ private fun WatchApp(baseApp: Note, nav: (String) -> Unit) {
|
||||
@Composable
|
||||
private fun DisplayBadges(
|
||||
baseUser: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
LoadAddressableNote(
|
||||
@@ -1182,20 +1181,29 @@ private fun DisplayBadges(
|
||||
baseUser.pubkeyHex,
|
||||
BadgeProfilesEvent.standardDTAg,
|
||||
null
|
||||
)
|
||||
) {
|
||||
if (it != null) {
|
||||
val badgeList by it.live().metadata.map {
|
||||
(it.note.event as? BadgeProfilesEvent)?.badgeAwardEvents()?.toImmutableList()
|
||||
}.distinctUntilChanged().observeAsState()
|
||||
|
||||
badgeList?.let { list ->
|
||||
RenderBadgeList(list, nav)
|
||||
}
|
||||
),
|
||||
accountViewModel
|
||||
) { note ->
|
||||
if (note != null) {
|
||||
WatchAndRenderBadgeList(note, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchAndRenderBadgeList(
|
||||
note: AddressableNote,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val badgeList by note.live().metadata.map {
|
||||
(it.note.event as? BadgeProfilesEvent)?.badgeAwardEvents()?.toImmutableList()
|
||||
}.distinctUntilChanged().observeAsState()
|
||||
|
||||
badgeList?.let { list ->
|
||||
RenderBadgeList(list, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
private fun RenderBadgeList(
|
||||
@@ -1215,7 +1223,7 @@ private fun RenderBadgeList(
|
||||
@Composable
|
||||
private fun LoadAndRenderBadge(badgeAwardEventHex: String, nav: (String) -> Unit) {
|
||||
var baseNote by remember {
|
||||
mutableStateOf<Note?>(LocalCache.getNoteIfExists(badgeAwardEventHex))
|
||||
mutableStateOf(LocalCache.getNoteIfExists(badgeAwardEventHex))
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = badgeAwardEventHex) {
|
||||
@@ -1460,7 +1468,7 @@ fun TabFollows(baseUser: User, feedViewModel: UserFeedViewModel, accountViewMode
|
||||
WatchFollowChanges(baseUser, feedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column() {
|
||||
Column {
|
||||
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false)
|
||||
}
|
||||
}
|
||||
@@ -1471,7 +1479,7 @@ fun TabFollowers(baseUser: User, feedViewModel: UserFeedViewModel, accountViewMo
|
||||
WatchFollowerChanges(baseUser, feedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column() {
|
||||
Column {
|
||||
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false)
|
||||
}
|
||||
}
|
||||
@@ -1506,7 +1514,7 @@ fun TabReceivedZaps(baseUser: User, zapFeedViewModel: NostrUserProfileZapsFeedVi
|
||||
WatchZapsAndUpdateFeed(baseUser, zapFeedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column() {
|
||||
Column {
|
||||
LnZapFeedView(zapFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
@@ -1529,7 +1537,7 @@ fun TabReports(baseUser: User, feedViewModel: NostrUserProfileReportFeedViewMode
|
||||
WatchReportsAndUpdateFeed(baseUser, feedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column() {
|
||||
Column {
|
||||
RefresheableFeedView(
|
||||
feedViewModel,
|
||||
null,
|
||||
@@ -1701,7 +1709,6 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); onDismiss() }) {
|
||||
Text(stringResource(R.string.copy_user_id))
|
||||
|
||||
@@ -56,7 +56,7 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss:
|
||||
Pair(ReportEvent.ReportType.ILLEGAL, stringResource(R.string.report_dialog_illegal))
|
||||
)
|
||||
|
||||
val reasonOptions = remember { reportTypes.map { it.second }.toImmutableList() }
|
||||
val reasonOptions = remember { reportTypes.map { TitleExplainer(it.second) }.toImmutableList() }
|
||||
var additionalReason by remember { mutableStateOf("") }
|
||||
var selectedReason by remember { mutableStateOf(-1) }
|
||||
|
||||
|
||||
@@ -111,15 +111,15 @@ fun SettingsScreen(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val selectedItens = persistentListOf(
|
||||
stringResource(ConnectivityType.ALWAYS.reourceId),
|
||||
stringResource(ConnectivityType.WIFI_ONLY.reourceId),
|
||||
stringResource(ConnectivityType.NEVER.reourceId)
|
||||
TitleExplainer(stringResource(ConnectivityType.ALWAYS.reourceId)),
|
||||
TitleExplainer(stringResource(ConnectivityType.WIFI_ONLY.reourceId)),
|
||||
TitleExplainer(stringResource(ConnectivityType.NEVER.reourceId))
|
||||
)
|
||||
|
||||
val themeItens = persistentListOf(
|
||||
stringResource(R.string.system),
|
||||
stringResource(R.string.light),
|
||||
stringResource(R.string.dark)
|
||||
TitleExplainer(stringResource(R.string.system)),
|
||||
TitleExplainer(stringResource(R.string.light)),
|
||||
TitleExplainer(stringResource(R.string.dark))
|
||||
)
|
||||
|
||||
val settings = accountViewModel.account.settings
|
||||
@@ -132,7 +132,7 @@ fun SettingsScreen(
|
||||
val context = LocalContext.current
|
||||
|
||||
val languageEntries = context.getLangPreferenceDropdownEntries()
|
||||
val languageList = languageEntries.keys.toImmutableList()
|
||||
val languageList = languageEntries.keys.map { TitleExplainer(it) }.toImmutableList()
|
||||
val languageIndex = getLanguageIndex(languageEntries)
|
||||
|
||||
Column(
|
||||
@@ -149,12 +149,12 @@ fun SettingsScreen(
|
||||
) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
val job = scope.launch(Dispatchers.IO) {
|
||||
val locale = languageEntries[languageList[it]]
|
||||
val locale = languageEntries[languageList[it].title]
|
||||
accountViewModel.account.settings.preferredLanguage = locale
|
||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||
}
|
||||
job.join()
|
||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]])
|
||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it].title])
|
||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||
}
|
||||
}
|
||||
@@ -227,7 +227,7 @@ fun SettingsScreen(
|
||||
fun SettingsRow(
|
||||
name: Int,
|
||||
description: Int,
|
||||
selectedItens: ImmutableList<String>,
|
||||
selectedItens: ImmutableList<TitleExplainer>,
|
||||
selectedIndex: Int,
|
||||
onSelect: (Int) -> Unit
|
||||
) {
|
||||
@@ -255,7 +255,7 @@ fun SettingsRow(
|
||||
|
||||
TextSpinner(
|
||||
label = "",
|
||||
placeholder = selectedItens[selectedIndex],
|
||||
placeholder = selectedItens[selectedIndex].title,
|
||||
options = selectedItens,
|
||||
onSelect = onSelect,
|
||||
modifier = Modifier
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillNode
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
@@ -321,10 +320,10 @@ fun LoginPage(
|
||||
|
||||
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
|
||||
Button(
|
||||
enabled = acceptedTerms.value,
|
||||
onClick = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
@@ -343,11 +342,7 @@ fun LoginPage(
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = if (acceptedTerms.value) MaterialTheme.colors.primary else Color.Gray
|
||||
)
|
||||
.height(50.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.login))
|
||||
}
|
||||
@@ -356,44 +351,43 @@ fun LoginPage(
|
||||
if (PackageUtils.isAmberInstalled(context)) {
|
||||
Box(modifier = Modifier.padding(40.dp, 40.dp, 40.dp, 0.dp)) {
|
||||
Button(
|
||||
enabled = acceptedTerms.value,
|
||||
onClick = {
|
||||
val result = ExternalSignerUtils.getDataFromResolver(SignerType.GET_PUBLIC_KEY, arrayOf("login"), "")
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||
return@Button
|
||||
}
|
||||
|
||||
val result = ExternalSignerUtils.getDataFromResolver(SignerType.GET_PUBLIC_KEY, arrayOf("login"))
|
||||
if (result == null) {
|
||||
loginWithExternalSigner = true
|
||||
return@Button
|
||||
}
|
||||
key.value = TextFieldValue(result)
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
} else {
|
||||
key.value = TextFieldValue(result)
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.key_is_required)
|
||||
return@Button
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = context.getString(R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
try {
|
||||
accountViewModel.startUI(
|
||||
key.value.text,
|
||||
useProxy.value,
|
||||
proxyPort.value.toInt(),
|
||||
true
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
try {
|
||||
accountViewModel.startUI(
|
||||
key.value.text,
|
||||
useProxy.value,
|
||||
proxyPort.value.toInt(),
|
||||
true
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = if (acceptedTerms.value) MaterialTheme.colors.primary else Color.Gray
|
||||
)
|
||||
.height(50.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.login_with_external_signer))
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.patrykandpatrick.vico.compose.style.ChartStyle
|
||||
@@ -347,11 +348,14 @@ fun AmethystTheme(themeViewModel: ThemeViewModel, content: @Composable () -> Uni
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
if (darkTheme) {
|
||||
window.statusBarColor = colors.background.toArgb()
|
||||
} else {
|
||||
window.statusBarColor = colors.primary.toArgb()
|
||||
}
|
||||
window.navigationBarColor = colors.background.toArgb()
|
||||
insetsController.isAppearanceLightNavigationBars = !darkTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,16 +48,16 @@
|
||||
<string name="in_channel">"চ্যানেলের ভেতর "</string>
|
||||
<string name="profile_banner">প্রোফাইলের ব্যানার</string>
|
||||
<string name="payment_successful">সফলভাবে মূল্য পরিশোধিত হয়েছে</string>
|
||||
<string name="error_parsing_error_message">এরর পার্সিং এরর মেসেজ</string>
|
||||
<string name="error_parsing_error_message">Error parsing error message</string>
|
||||
<string name="following">" অনুসরণ"</string>
|
||||
<string name="followers">" অনুসারী"</string>
|
||||
<string name="profile">প্রোফাইল</string>
|
||||
<string name="security_filters">নিরাপত্তা-ফিল্টার</string>
|
||||
<string name="log_out">লগ আউট</string>
|
||||
<string name="show_more">আরও দেখান</string>
|
||||
<string name="lightning_invoice">লাইটনিং ইনভয়েস</string>
|
||||
<string name="lightning_invoice">বিজলি-ইনভয়েস</string>
|
||||
<string name="pay">পরিশোধ করুন</string>
|
||||
<string name="lightning_tips">লাইটনিং টিপস</string>
|
||||
<string name="lightning_tips">বিজলি-বকশিশ</string>
|
||||
<string name="note_to_receiver">প্রাপকের উদ্দেশে চিরকুট</string>
|
||||
<string name="thank_you_so_much">আপনাকে অনেক ধন্যবাদ!</string>
|
||||
<string name="amount_in_sats">স্যাটে মোট পরিমাণ</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<string name="create">তৈরি করুন</string>
|
||||
<string name="cancel">বাতিল করুন</string>
|
||||
<string name="failed_to_upload_the_image">ছবিটি সফলভাবে আপলোড করা যায় নি</string>
|
||||
<string name="relay_address">রিলে ঠিকানা</string>
|
||||
<string name="relay_address">রিলে-ঠিকানা</string>
|
||||
<string name="posts">পোস্টগুলি</string>
|
||||
<string name="bytes">বাইটস</string>
|
||||
<string name="errors">ত্রুটিসমূহ</string>
|
||||
@@ -94,13 +94,13 @@
|
||||
<string name="avatar_url">অবতারের URL</string>
|
||||
<string name="banner_url">ব্যানারের URL</string>
|
||||
<string name="website_url">ওয়েবসাইটের URL</string>
|
||||
<string name="ln_address">LN ঠিকানা</string>
|
||||
<string name="ln_url_outdated">LN URL (অপ্রচলিত)</string>
|
||||
<string name="ln_address">বিজলি-ঠিকানা</string>
|
||||
<string name="ln_url_outdated">বিজলি URL (অপ্রচলিত)</string>
|
||||
<string name="image_saved_to_the_gallery">ছবিটি গ্যালারিতে সংরক্ষণ করা হয়েছে</string>
|
||||
<string name="failed_to_save_the_image">ছবিটি সফলভাবে সংরক্ষণ করা যায় নি</string>
|
||||
<string name="upload_image">ছবিটি আপলোড করুন</string>
|
||||
<string name="uploading">আপলোড করা হচ্ছে…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">স্যাট গ্রহণের জন্য ব্যবহারকারীর কোনো লাইটনিং ঠিকানা দেওয়া নেই</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">স্যাট গ্রহণের জন্য ব্যবহারকারীর কোনো বিজলি-ঠিকানা দেওয়া নেই</string>
|
||||
<string name="reply_here">"এখানে প্রতিউত্তর দিন.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">নস্টারে শেয়ারের জন্য নোট আইডিটি ক্লিপবোর্ডে কপি করা হয়</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">চ্যানেল আইডি (নোট) ক্লিপবোর্ডে কপি করুন</string>
|
||||
@@ -118,7 +118,7 @@
|
||||
<string name="more_options">আরও বিকল্প</string>
|
||||
<string name="relays">" রিলেগুলি"</string>
|
||||
<string name="website">ওয়েবসাইট</string>
|
||||
<string name="lightning_address">লাইটনিং ঠিকানা</string>
|
||||
<string name="lightning_address">বিজলি-ঠিকানা</string>
|
||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">ব্যাকআপের জন্য আপনার Nsec আইডি (তথা পাসওয়ার্ড) ক্লিপবোর্ডে কপি করে</string>
|
||||
<string name="copy_private_key_to_the_clipboard">আপনার ব্যক্তিগত চাবিটি ক্লিপবোর্ডে কপি করুন</string>
|
||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">অন্যদের সাথে শেয়ারের জন্য আপনার গণ চাবি ক্লিপবোর্ডে কপি করে</string>
|
||||
@@ -130,23 +130,23 @@
|
||||
<string name="unblock">আনব্লক করুন</string>
|
||||
<string name="copy_user_id">ব্যবহারকারীর আইডি কপি করুন</string>
|
||||
<string name="unblock_user">ব্যবহারকারীকে আনব্লক করুন</string>
|
||||
<string name="npub_hex_username">"Npub, ইউজারনেম, টেক্সট"</string>
|
||||
<string name="npub_hex_username">"গণচাবি, ইউজারনেম, টেক্সট"</string>
|
||||
<string name="clear">মুছে ফেলুন</string>
|
||||
<string name="app_logo">অ্যাপের লোগো</string>
|
||||
<string name="nsec_npub_hex_private_key">Nsec.. অথবা Npub..</string>
|
||||
<string name="show_password">পাসওয়ার্ড দেখান</string>
|
||||
<string name="hide_password">পাসওয়ার্ড লুকান</string>
|
||||
<string name="invalid_key">অকেজো কি</string>
|
||||
<string name="invalid_key">অকেজো চাবি</string>
|
||||
<string name="i_accept_the">"আমি এটা মেনে নিচ্ছি "</string>
|
||||
<string name="terms_of_use">ব্যবহারের শর্তাবলী</string>
|
||||
<string name="acceptance_of_terms_is_required">প্রদত্ত শর্তাবলী কবুল করা আবশ্যক</string>
|
||||
<string name="key_is_required">কি প্রয়োজন</string>
|
||||
<string name="key_is_required">চাবি প্রয়োজন</string>
|
||||
<string name="login">লগ ইন</string>
|
||||
<string name="generate_a_new_key">একটি নতুন কি তৈরি করুন</string>
|
||||
<string name="loading_feed">ফিডটি লোড করা হচ্ছে</string>
|
||||
<string name="generate_a_new_key">একটি নতুন চাবি তৈরি করুন</string>
|
||||
<string name="loading_feed">পাতাটি লোড করা হচ্ছে</string>
|
||||
<string name="error_loading_replies">"প্রতিউত্তরগুলি লোড করা যাচ্ছে না: "</string>
|
||||
<string name="try_again">আবার চেষ্টা করুন</string>
|
||||
<string name="feed_is_empty">ফিডটি খালি.</string>
|
||||
<string name="feed_is_empty">পাতাটি খালি</string>
|
||||
<string name="refresh">রিফ্রেশ করুন</string>
|
||||
<string name="created">তৈরি করা হয়েছে</string>
|
||||
<string name="with_description_of">এর বিবরণ সহ</string>
|
||||
@@ -165,7 +165,7 @@
|
||||
<string name="translations_translated_from">এই মূলপাঠ থেকে অনূদিত হয়েছে</string>
|
||||
<string name="translations_to">থেকে</string>
|
||||
<string name="translations_show_in_lang_first">প্রথমে %1$s ভাষায় দেখান</string>
|
||||
<string name="translations_always_translate_to_lang">সর্বদা %1$s ভাষায় অনুবাদ করুন</string>
|
||||
<string name="translations_always_translate_to_lang">সবসময় %1$s ভাষায় অনুবাদ করুন</string>
|
||||
<string name="translations_never_translate_from_lang">কখনোই %1$s ভাষা থেকে অনুবাদ করবেন না</string>
|
||||
<string name="nip_05">নস্টার ঠিকানা</string>
|
||||
<string name="never">কখনো না</string>
|
||||
@@ -250,7 +250,7 @@
|
||||
<string name="remove_from_private_bookmarks">ব্যক্তিগত বুকমার্ক থেকে সরিয়ে দিন</string>
|
||||
<string name="remove_from_public_bookmarks">সর্বজনীন বুকমার্ক থেকে সরিয়ে দিন</string>
|
||||
<string name="wallet_connect_service">ওয়ালেট সংযোগের পরিষেবা</string>
|
||||
<string name="wallet_connect_service_explainer">অ্যাপ থেকে বের না হয়েই জ্যাপ প্রদান করতে একটি নস্টার-সিক্রেট অনুমোদন করা হয়ে থাকে। এই নস্টার-সিক্রেটটি নিরাপদে রাখুন এবং সম্ভব হলে একটি প্রাইভেট রিলে ব্যবহার করুন</string>
|
||||
<string name="wallet_connect_service_explainer">অ্যাপ থেকে বের না হয়েই জ্যাপ প্রদান করতে একটি নস্টার-সিক্রেট অনুমোদন করা হয়ে থাকে। এই নস্টার-সিক্রেটটি নিরাপদে রাখুন এবং সম্ভব হলে একটি ব্যক্তিগত রিলে ব্যবহার করুন</string>
|
||||
<string name="wallet_connect_service_pubkey">ওয়ালেট সংযোগের গণ চাবি</string>
|
||||
<string name="wallet_connect_service_relay">ওয়ালেট সংযোগের রিলে</string>
|
||||
<string name="wallet_connect_service_secret">ওয়ালেট সংযোগের সিক্রেট</string>
|
||||
@@ -297,9 +297,9 @@
|
||||
<string name="zap_type_anonymous">অজ্ঞাত-পরিচয়</string>
|
||||
<string name="zap_type_anonymous_explainer">প্রাপক এবং জনসাধারণ জানেন না কে প্রদত্ত অর্থ পাঠিয়েছেন</string>
|
||||
<string name="zap_type_nonzap">নন-জ্যাপ</string>
|
||||
<string name="zap_type_nonzap_explainer">নস্টারে কোনো সন্ধান নেই, কেবল লাইটনিংয়ে আছে</string>
|
||||
<string name="zap_type_nonzap_explainer">নস্টারে কোনো সন্ধান নেই, কেবল বিজলিতে আছে</string>
|
||||
<string name="file_server">ফাইল সার্ভার</string>
|
||||
<string name="zap_forward_lnAddress">LN ঠিকানা বা @ইউজার</string>
|
||||
<string name="zap_forward_lnAddress">বিজলি-ঠিকানা বা @ব্যবহারকারী</string>
|
||||
<string name="upload_server_imgur">imgur.com - বিশ্বস্ত</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur ফাইলটি পরিবর্তন করতে পারে</string>
|
||||
<string name="upload_server_nostrimg">nostrimg.com - বিশ্বস্ত</string>
|
||||
@@ -398,7 +398,7 @@
|
||||
<string name="payment">মূল্যপরিশোধ</string>
|
||||
<string name="cashu">ক্যাশু টোকেন</string>
|
||||
<string name="cashu_redeem">খালাস করুন</string>
|
||||
<string name="no_lightning_address_set">কোনো লাইটনিং ঠিকানা সেট করা নেই</string>
|
||||
<string name="no_lightning_address_set">কোনো বিজলি-ঠিকানা সেট করা নেই</string>
|
||||
<string name="copied_token_to_clipboard">টোকেনটি ক্লিপবোর্ডে কপি করা হয়েছে</string>
|
||||
<string name="live_stream_live_tag">লাইভ</string>
|
||||
<string name="live_stream_offline_tag">অফলাইন</string>
|
||||
@@ -414,7 +414,7 @@
|
||||
<string name="discover_chat">বার্তালাপগুলি</string>
|
||||
<string name="community_approved_posts">অনুমোদিত পোস্টগুলি</string>
|
||||
<string name="groups_no_descriptor">এই দলটির কোনো বিবরণ কিংবা নীতিমালা নেই। এগুলো যুক্ত করতে দলনেতার সাথে কথা বলুন</string>
|
||||
<string name="community_no_descriptor">এই কম্যুনিটির কোনো বিবরণ কিংবা নীতিমালা নেই। এগুলো যুক্ত করতে এর মালিকের সাথে কথা বলুন</string>
|
||||
<string name="community_no_descriptor">এই গোষ্ঠীর কোনো বিবরণ কিংবা নীতিমালা নেই। এগুলো যুক্ত করতে এর মালিকের সাথে কথা বলুন</string>
|
||||
<string name="add_sensitive_content_label">সংবেদনশীল আধেয়</string>
|
||||
<string name="add_sensitive_content_description">সংবেদনশীল আধেয় দেখানোর আগে এ সম্পর্কিত সতর্কতা প্রদর্শন করে</string>
|
||||
<string name="settings">সেটিংস</string>
|
||||
@@ -442,7 +442,7 @@
|
||||
<string name="default_relays">পূর্ব-নির্ধারিত</string>
|
||||
<string name="select_a_relay_to_continue">চালিয়ে যেতে একটি রিলে নির্বাচন করুন</string>
|
||||
<string name="zap_forward_title">জ্যাপটি পাঠান:</string>
|
||||
<string name="zap_forward_explainer">সহায়তাকারী ক্লায়েন্টরা আপনার পরিবর্তে নীচের LN ঠিকানা বা ব্যবহারকারীর প্রোফাইলে জ্যাপ পাঠাবে</string>
|
||||
<string name="zap_forward_explainer">সহায়তাকারী ক্লায়েন্টরা আপনার পরিবর্তে নিচের বিজলি-ঠিকানায় বা ব্যবহারকারীর প্রোফাইলে জ্যাপ পাঠাবে</string>
|
||||
<string name="geohash_title">আপনার অবস্থান এভাবে প্রকাশ করুন </string>
|
||||
<string name="geohash_explainer">পোস্টে আপনার অবস্থানের একটি জিওহ্যাশ যোগ করে। জনসাধারণ জানবে যে আপনি বর্তমান অবস্থান থেকে ৫ কিলোমিটার (৩ মাইল) এর মধ্যে আছেন</string>
|
||||
<string name="add_sensitive_content_explainer">আপনার পেশকৃত আধেয় দেখানোর আগে সংবেদনশীল আধেয়-সতর্কতা জারি করে। এটি সেসকল আধেয়র জন্য আদর্শ যেখানে NSFW উপাদান কিংবা এমনকিছু থাকতে পারে যা কারো কারো জন্য আপত্তিকর কিংবা সমস্যাজনক মনে হতে পারে।</string>
|
||||
@@ -458,6 +458,7 @@
|
||||
<string name="messages_group_descriptor">এই দলের সদস্যরা</string>
|
||||
<string name="messages_new_subject_message">সদস্যদের নিকট ব্যাখ্যা</string>
|
||||
<string name="messages_new_subject_message_placeholder">নতুন লক্ষ্যের জন্য নাম পরিবর্তন করা হচ্ছে।</string>
|
||||
<string name="paste_from_clipboard">ক্লিপবোর্ড থেকে পেস্ট করুন</string>
|
||||
<string name="language_description">অ্যাপের ইন্টারফেসের জন্য</string>
|
||||
<string name="theme_description">আঁধারি, উজ্জ্বল কিংবা সিস্টেমের থিম</string>
|
||||
<string name="automatically_load_images_gifs_description">স্বয়ংক্রিয়ভাবে ছবি এবং GIF লোড করুন</string>
|
||||
@@ -468,6 +469,7 @@
|
||||
<string name="copy_the_note_id_to_the_clipboard">নোট আইডি ক্লিপবোর্ডে কপি করুন</string>
|
||||
<string name="created_at">যেখানে নির্মিত</string>
|
||||
<string name="rules">নিয়ম</string>
|
||||
<string name="login_with_external_signer">Amber দিয়ে লগইন করুন</string>
|
||||
<string name="status_update">আপনার স্ট্যাটাস আপডেট করুন</string>
|
||||
<string name="lightning_wallets_not_found">Error parsing error message</string>
|
||||
<string name="poll_zap_value_min_max_explainer">জ্যাপের পরিমাণ দ্বারা ভোটসংখ্যা গোণা হয়। আপনি স্প্যামার এড়াতে একটি সর্বনিম্ন পরিমাণ এবং একটি বড়ো জ্যাপার দ্বারা পোল দখল এড়াতে সর্বোচ্চ পরিমাণ স্যাট নির্ধারণ করতে পারেন। প্রতিটি ভোটের মূল্য একই পরিমাণে রয়েছে তা নিশ্চিত করতে উভয় ক্ষেত্রেই একই পরিমাণ ব্যবহার করুন। যেকোনো পরিমাণ গ্রহণ করতে এটি খালি ছেড়ে দিন।</string>
|
||||
@@ -488,12 +490,14 @@
|
||||
<string name="zap_split_explainer">সমর্থিত ক্লায়েন্টরা জ্যাপ ভাগ করবে এবং আপনার পরিবর্তে এখানে সংযুক্ত ব্যবহারকারীদেরকে পাঠাবে</string>
|
||||
<string name="zap_split_serarch_and_add_user">ব্যবহারকারীদের অনুসন্ধান করুন এবং যুক্ত করুন</string>
|
||||
<string name="zap_split_serarch_and_add_user_placeholder">ইউজারনেম কিংবা প্রদর্শিত নাম দিন</string>
|
||||
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">%1$s ব্যবহারকারীর স্যাট গ্রহণের উপযোগী কোনো লাইটনিং ঠিকানা সেট করা নেই</string>
|
||||
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">%1$s ব্যবহারকারীর স্যাট গ্রহণের উপযোগী কোনো বিজলি ঠিকানা সেট করা নেই</string>
|
||||
<string name="zap_split_weight">শতাংশ</string>
|
||||
<string name="zap_split_weight_placeholder">২৫</string>
|
||||
<string name="splitting_zaps_with">এর সাথে জ্যাপ ভাগ করা হচ্ছে</string>
|
||||
<string name="forwarding_zaps_to">একে জ্যাপ পাঠানো হচ্ছে</string>
|
||||
<string name="lightning_wallets_not_found2">লাইটনিং ওয়ালেট পাওয়া যায় নি</string>
|
||||
<string name="lightning_wallets_not_found2">বিজলি-ওয়ালেট পাওয়া যায় নি</string>
|
||||
<string name="paid">পরিশোধিত</string>
|
||||
<string name="wallet_number">ওয়ালেট %1$s</string>
|
||||
<string name="error_opening_external_signer">স্বাক্ষরের অ্যাপ খোলা যাচ্ছে না</string>
|
||||
<string name="sign_request_rejected">স্বাক্ষরের অনুরোধ প্রত্যাখ্যান করা হয়েছে</string>
|
||||
</resources>
|
||||
|
||||
@@ -473,7 +473,6 @@
|
||||
<string name="poll_zap_value_min_max_explainer">I voti sono ponderati con l\'importo dello zap. È possibile impostare una quantità minima per evitare gli spammer e una quantità massima per evitare che un grande zapper predomini il sondaggio. Usa lo stesso importo in entrambi i campi per assicurarti che ogni voto valga lo stesso importo. Lascialo vuoto per accettare qualsiasi importo.</string>
|
||||
<string name="error_dialog_zap_error">Impossibile inviare lo zap</string>
|
||||
<string name="error_dialog_talk_to_user">Invia un messaggio all\'utente</string>
|
||||
<string name="error_dialog_button_ok">OK</string>
|
||||
<string name="relay_information_document_error_assemble_url">Impossibile raggiungere %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Impossibile raggiungere %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_parse_result">Impossibile analizzare il risultato da %1$s: %2$s</string>
|
||||
|
||||
@@ -458,6 +458,7 @@
|
||||
<string name="messages_group_descriptor">Leden van deze groep</string>
|
||||
<string name="messages_new_subject_message">Uitleg aan leden</string>
|
||||
<string name="messages_new_subject_message_placeholder">De naam veranderen voor de nieuwe doelen.</string>
|
||||
<string name="paste_from_clipboard">Plakken vanaf klembord</string>
|
||||
<string name="language_description">Voor de interface van de app</string>
|
||||
<string name="theme_description">Donker-, Licht- of Systeemthema</string>
|
||||
<string name="automatically_load_images_gifs_description">Automatisch afbeeldingen en GIF\'s laden</string>
|
||||
@@ -468,6 +469,7 @@
|
||||
<string name="copy_the_note_id_to_the_clipboard">Note naar klembord kopiëren</string>
|
||||
<string name="created_at">Gemaakt op</string>
|
||||
<string name="rules">Regels</string>
|
||||
<string name="login_with_external_signer">Login met Amber</string>
|
||||
<string name="status_update">Status bijwerken</string>
|
||||
<string name="lightning_wallets_not_found">Fout bij het parsen van foutberichten</string>
|
||||
<string name="poll_zap_value_min_max_explainer">Stemmen worden gewogen door het aantal zap. U kunt een minimumbedrag instellen om spammers te voorkomen en een maximumbedrag om te voorkomen dat een grote zappers de enquête overneemt. Gebruik hetzelfde bedrag in beide velden om ervoor te zorgen dat elke stem hetzelfde wordt gewaardeerd. Laat het leeg om elk bedrag te accepteren.</string>
|
||||
@@ -496,4 +498,6 @@
|
||||
<string name="lightning_wallets_not_found2">Lightning wallets niet gevonden</string>
|
||||
<string name="paid">Betaald</string>
|
||||
<string name="wallet_number">Wallet %1$s</string>
|
||||
<string name="error_opening_external_signer">Fout bij openen ondertekenapp</string>
|
||||
<string name="sign_request_rejected">Ondertekenverzoek afgewezen</string>
|
||||
</resources>
|
||||
|
||||
@@ -561,7 +561,7 @@
|
||||
|
||||
<string name="error_dialog_zap_error">Unable to send zap</string>
|
||||
<string name="error_dialog_talk_to_user">Message the User</string>
|
||||
<string name="error_dialog_button_ok">Ok</string>
|
||||
<string name="error_dialog_button_ok">OK</string>
|
||||
|
||||
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_reach_server">Failed to reach %1$s: %2$s</string>
|
||||
|
||||
+4
-16
@@ -14,7 +14,6 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -42,14 +41,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
@Immutable
|
||||
data class TranslationConfig(
|
||||
val result: String?,
|
||||
val sourceLang: String?,
|
||||
val targetLang: String?,
|
||||
val showOriginal: Boolean
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun TranslatableRichTextViewer(
|
||||
content: String,
|
||||
@@ -312,18 +303,17 @@ private fun TranslationMessage(
|
||||
@Composable
|
||||
fun TranslateAndWatchLanguageChanges(content: String, accountViewModel: AccountViewModel, onTranslated: (TranslationConfig) -> Unit) {
|
||||
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
LaunchedEffect(accountState) {
|
||||
launch(Dispatchers.IO) {
|
||||
accountViewModel.runOnIO {
|
||||
LanguageTranslatorService.autoTranslate(
|
||||
content,
|
||||
account.dontTranslateFrom,
|
||||
account.translateTo
|
||||
accountViewModel.account.dontTranslateFrom,
|
||||
accountViewModel.account.translateTo
|
||||
).addOnCompleteListener { task ->
|
||||
if (task.isSuccessful && !content.equals(task.result.result, true)) {
|
||||
if (task.result.sourceLang != null && task.result.targetLang != null) {
|
||||
val preference = account.preferenceBetween(task.result.sourceLang!!, task.result.targetLang!!)
|
||||
val preference = accountViewModel.account.preferenceBetween(task.result.sourceLang!!, task.result.targetLang!!)
|
||||
val newConfig = TranslationConfig(
|
||||
result = task.result.result,
|
||||
sourceLang = task.result.sourceLang,
|
||||
@@ -331,9 +321,7 @@ fun TranslateAndWatchLanguageChanges(content: String, accountViewModel: AccountV
|
||||
showOriginal = preference == task.result.sourceLang
|
||||
)
|
||||
|
||||
// withContext(Dispatchers.Main) {
|
||||
onTranslated(newConfig)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.Dao
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -11,9 +14,23 @@ import org.junit.Test
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class NewMessageTaggerKeyParseTest {
|
||||
val dao: Dao = object : Dao {
|
||||
override suspend fun getOrCreateUser(hex: String): User {
|
||||
return User(hex)
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateNote(hex: String): Note {
|
||||
return Note(hex)
|
||||
}
|
||||
|
||||
override suspend fun checkGetOrCreateAddressableNote(hex: String): Note? {
|
||||
return Note(hex)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyParseTestNote() {
|
||||
val result = NewMessageTagger(message = "").parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn")
|
||||
val result = NewMessageTagger(message = "", dao = dao).parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn")
|
||||
assertEquals(Nip19.Type.NOTE, result?.key?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.key?.hex)
|
||||
assertEquals("", result?.restOfWord)
|
||||
@@ -21,7 +38,7 @@ class NewMessageTaggerKeyParseTest {
|
||||
|
||||
@Test
|
||||
fun keyParseTestPub() {
|
||||
val result = NewMessageTagger(message = "").parseDirtyWordForKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
|
||||
val result = NewMessageTagger(message = "", dao = dao).parseDirtyWordForKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
|
||||
assertEquals(Nip19.Type.USER, result?.key?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.key?.hex)
|
||||
assertEquals("", result?.restOfWord)
|
||||
@@ -29,7 +46,7 @@ class NewMessageTaggerKeyParseTest {
|
||||
|
||||
@Test
|
||||
fun keyParseTestNoteWithExtraChars() {
|
||||
val result = NewMessageTagger(message = "").parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
val result = NewMessageTagger(message = "", dao = dao).parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
assertEquals(Nip19.Type.NOTE, result?.key?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
@@ -37,7 +54,7 @@ class NewMessageTaggerKeyParseTest {
|
||||
|
||||
@Test
|
||||
fun keyParseTestPubWithExtraChars() {
|
||||
val result = NewMessageTagger(message = "").parseDirtyWordForKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
val result = NewMessageTagger(message = "", dao = dao).parseDirtyWordForKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
assertEquals(Nip19.Type.USER, result?.key?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
@@ -45,7 +62,7 @@ class NewMessageTaggerKeyParseTest {
|
||||
|
||||
@Test
|
||||
fun keyParseTestNoteWithExtraCharsAndAt() {
|
||||
val result = NewMessageTagger(message = "").parseDirtyWordForKey("@note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
val result = NewMessageTagger(message = "", dao = dao).parseDirtyWordForKey("@note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
assertEquals(Nip19.Type.NOTE, result?.key?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
@@ -53,7 +70,7 @@ class NewMessageTaggerKeyParseTest {
|
||||
|
||||
@Test
|
||||
fun keyParseTestPubWithExtraCharsAndAt() {
|
||||
val result = NewMessageTagger(message = "").parseDirtyWordForKey("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
val result = NewMessageTagger(message = "", dao = dao).parseDirtyWordForKey("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
assertEquals(Nip19.Type.USER, result?.key?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
@@ -61,7 +78,7 @@ class NewMessageTaggerKeyParseTest {
|
||||
|
||||
@Test
|
||||
fun keyParseTestNoteWithExtraCharsAndNostrPrefix() {
|
||||
val result = NewMessageTagger(message = "").parseDirtyWordForKey("nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
val result = NewMessageTagger(message = "", dao = dao).parseDirtyWordForKey("nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
assertEquals(Nip19.Type.NOTE, result?.key?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
@@ -69,7 +86,7 @@ class NewMessageTaggerKeyParseTest {
|
||||
|
||||
@Test
|
||||
fun keyParseTestPubWithExtraCharsAndNostrPrefix() {
|
||||
val result = NewMessageTagger(message = "").parseDirtyWordForKey("nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
val result = NewMessageTagger(message = "", dao = dao).parseDirtyWordForKey("nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
assertEquals(Nip19.Type.USER, result?.key?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
@@ -77,7 +94,7 @@ class NewMessageTaggerKeyParseTest {
|
||||
|
||||
@Test
|
||||
fun keyParseTestUppercaseNoteWithExtraCharsAndNostrPrefix() {
|
||||
val result = NewMessageTagger(message = "").parseDirtyWordForKey("Nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
val result = NewMessageTagger(message = "", dao = dao).parseDirtyWordForKey("Nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
assertEquals(Nip19.Type.NOTE, result?.key?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
@@ -85,7 +102,7 @@ class NewMessageTaggerKeyParseTest {
|
||||
|
||||
@Test
|
||||
fun keyParseTestUppercasePubWithExtraCharsAndNostrPrefix() {
|
||||
val result = NewMessageTagger(message = "").parseDirtyWordForKey("nOstr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
val result = NewMessageTagger(message = "", dao = dao).parseDirtyWordForKey("nOstr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
assertEquals(Nip19.Type.USER, result?.key?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ buildscript {
|
||||
core_ktx_version = '1.12.0'
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.google.gms:google-services:4.3.15'
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,16 +15,28 @@ class AppDefinitionEvent(
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
fun appMetaData() = try {
|
||||
mapper.readValue(
|
||||
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
|
||||
UserMetadata::class.java
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.w("MT", "Content Parse Error ${e.localizedMessage} $content")
|
||||
null
|
||||
}
|
||||
@Transient
|
||||
private var cachedMetadata: UserMetadata? = null
|
||||
|
||||
fun appMetaData() =
|
||||
if (cachedMetadata != null) {
|
||||
cachedMetadata
|
||||
} else {
|
||||
try {
|
||||
val newMetadata = mapper.readValue(
|
||||
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
|
||||
UserMetadata::class.java
|
||||
)
|
||||
|
||||
cachedMetadata = newMetadata
|
||||
|
||||
newMetadata
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.w("MT", "Content Parse Error ${e.localizedMessage} $content")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun supportedKinds() = tags.filter { it.size > 1 && it[0] == "k" }.mapNotNull {
|
||||
runCatching { it[1].toInt() }.getOrNull()
|
||||
|
||||
@@ -350,6 +350,8 @@ class UserMetadata {
|
||||
@Stable
|
||||
data class ImmutableListOfLists<T>(val lists: List<List<T>> = emptyList())
|
||||
|
||||
val EmptyTagList = ImmutableListOfLists<String>(emptyList())
|
||||
|
||||
fun List<List<String>>.toImmutableListOfLists(): ImmutableListOfLists<String> {
|
||||
return ImmutableListOfLists(this)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class FileHeaderEvent(
|
||||
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
||||
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
||||
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
|
||||
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
|
||||
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
|
||||
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
|
||||
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
|
||||
@@ -41,11 +42,12 @@ class FileHeaderEvent(
|
||||
private const val MAGNET_URI = "magnet"
|
||||
private const val TORRENT_INFOHASH = "i"
|
||||
private const val BLUR_HASH = "blurhash"
|
||||
private const val ALT = "alt"
|
||||
|
||||
fun create(
|
||||
url: String,
|
||||
mimeType: String? = null,
|
||||
description: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
@@ -60,11 +62,13 @@ class FileHeaderEvent(
|
||||
val tags = listOfNotNull(
|
||||
listOf(URL, url),
|
||||
mimeType?.let { listOf(MIME_TYPE, mimeType) },
|
||||
alt?.let { listOf(ALT, it) },
|
||||
hash?.let { listOf(HASH, it) },
|
||||
size?.let { listOf(FILE_SIZE, it) },
|
||||
dimensions?.let { listOf(DIMENSION, it) },
|
||||
blurhash?.let { listOf(BLUR_HASH, it) },
|
||||
magnetURI?.let { listOf(MAGNET_URI, it) },
|
||||
|
||||
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
|
||||
encryptionKey?.let { listOf(ENCRYPTION_KEY, it.key, it.nonce) },
|
||||
sensitiveContent?.let {
|
||||
@@ -76,7 +80,7 @@ class FileHeaderEvent(
|
||||
}
|
||||
)
|
||||
|
||||
val content = description ?: ""
|
||||
val content = alt ?: ""
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
|
||||
|
||||
@@ -22,6 +22,7 @@ class FileStorageHeaderEvent(
|
||||
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
||||
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
||||
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
|
||||
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
|
||||
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
|
||||
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
|
||||
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
|
||||
@@ -38,11 +39,12 @@ class FileStorageHeaderEvent(
|
||||
private const val MAGNET_URI = "magnet"
|
||||
private const val TORRENT_INFOHASH = "i"
|
||||
private const val BLUR_HASH = "blurhash"
|
||||
private const val ALT = "alt"
|
||||
|
||||
fun create(
|
||||
storageEvent: FileStorageEvent,
|
||||
mimeType: String? = null,
|
||||
description: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
@@ -57,6 +59,7 @@ class FileStorageHeaderEvent(
|
||||
val tags = listOfNotNull(
|
||||
listOf("e", storageEvent.id),
|
||||
mimeType?.let { listOf(MIME_TYPE, mimeType) },
|
||||
alt?.let { listOf(ALT, it) },
|
||||
hash?.let { listOf(HASH, it) },
|
||||
size?.let { listOf(FILE_SIZE, it) },
|
||||
dimensions?.let { listOf(DIMENSION, it) },
|
||||
@@ -73,7 +76,7 @@ class FileStorageHeaderEvent(
|
||||
}
|
||||
)
|
||||
|
||||
val content = description ?: ""
|
||||
val content = alt ?: ""
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
return FileStorageHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, "")
|
||||
}
|
||||
@@ -81,7 +84,7 @@ class FileStorageHeaderEvent(
|
||||
fun create(
|
||||
storageEvent: FileStorageEvent,
|
||||
mimeType: String? = null,
|
||||
description: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
@@ -97,6 +100,7 @@ class FileStorageHeaderEvent(
|
||||
listOf("e", storageEvent.id),
|
||||
mimeType?.let { listOf(MIME_TYPE, mimeType) },
|
||||
hash?.let { listOf(HASH, it) },
|
||||
alt?.let { listOf(ALT, it) },
|
||||
size?.let { listOf(FILE_SIZE, it) },
|
||||
dimensions?.let { listOf(DIMENSION, it) },
|
||||
blurhash?.let { listOf(BLUR_HASH, it) },
|
||||
@@ -112,7 +116,7 @@ class FileStorageHeaderEvent(
|
||||
}
|
||||
)
|
||||
|
||||
val content = description ?: ""
|
||||
val content = alt ?: ""
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
|
||||
Reference in New Issue
Block a user