mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-04 13:54:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d36f6f4a7e | ||
|
|
c2b72f1e45 | ||
|
|
28e6471ade | ||
|
|
c3b8eb5087 | ||
|
|
75779e7ddc | ||
|
|
118bc7b73c | ||
|
|
e6a0fdedd9 | ||
|
|
28098fafd7 | ||
|
|
0c0e87af21 | ||
|
|
8abfd7149b | ||
|
|
674896cea4 | ||
|
|
ec514651fc | ||
|
|
8071d48911 | ||
|
|
69daa3835b | ||
|
|
57dac276b2 | ||
|
|
3a84595aac | ||
|
|
7b591c84f8 | ||
|
|
e8c8167451 | ||
|
|
ec93fc53b2 | ||
|
|
ba2902b725 | ||
|
|
e7defa14b9 | ||
|
|
4fbc3f25b3 | ||
|
|
4ae88d5cc8 | ||
|
|
1aea0720df | ||
|
|
846f340f84 | ||
|
|
cb137205c0 | ||
|
|
f910709e1b | ||
|
|
1d78dd6b19 | ||
|
|
3197be8a75 | ||
|
|
91ed4128fe | ||
|
|
f415490fcd | ||
|
|
4bdb5a28d0 | ||
|
|
58efe0b3c0 | ||
|
|
da3cecd324 | ||
|
|
09b1451dc1 | ||
|
|
9ae31a3429 | ||
|
|
b5258f89c1 | ||
|
|
fcc67fc4f2 | ||
|
|
579465ac6c | ||
|
|
6cbee81e72 | ||
|
|
ad683ed444 | ||
|
|
c53b1571a3 | ||
|
|
2c19b2ffef | ||
|
|
3694d1b0f5 | ||
|
|
e7a4746d22 | ||
|
|
7961f6f065 | ||
|
|
7c1c500990 |
@@ -85,7 +85,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-universal-release-unsigned-signed.apk
|
||||
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
|
||||
asset_content_type: application/zip
|
||||
|
||||
+5
-5
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 267
|
||||
versionName "0.72.0"
|
||||
versionCode 269
|
||||
versionName "0.72.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -25,9 +25,9 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Make sure all of JSON parsers work when activating these.
|
||||
//minifyEnabled true
|
||||
//proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
//signingConfig signingConfigs.debug
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), 'proguard-rules.pro'
|
||||
minifyEnabled true
|
||||
resValue "string", "app_name", "@string/app_name_release"
|
||||
}
|
||||
debug {
|
||||
|
||||
Vendored
+40
-25
@@ -5,33 +5,48 @@
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
# For the Secp256k1 library
|
||||
# preserve the line number information for debugging stack traces.
|
||||
-dontobfuscate
|
||||
-keepattributes LocalVariableTable
|
||||
-keepattributes LocalVariableTypeTable
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes SourceFile
|
||||
-keepattributes LineNumberTable
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
-keepattributes InnerClasses
|
||||
-keepattributes EnclosingMethod
|
||||
-keepattributes MethodParameters
|
||||
-keepparameternames
|
||||
|
||||
-keep class fr.acinq.secp256k1.jni.** { *; }
|
||||
# For the NostrPostr library
|
||||
-keep class nostr.postr.** { *; }
|
||||
-keepdirectories libs
|
||||
|
||||
# Keep all names
|
||||
-keepnames class ** { *; }
|
||||
|
||||
# Keep All enums
|
||||
-keep enum ** { *; }
|
||||
|
||||
# preserve access to native classses
|
||||
-keep class fr.acinq.secp256k1.** { *; }
|
||||
|
||||
# GSON parsing
|
||||
-keep class com.vitorpamplona.amethyst.service.model.** { *; }
|
||||
# Json parsing
|
||||
-keep class com.google.gson.reflect.** { *; }
|
||||
-keep class * extends com.google.gson.reflect.TypeToken
|
||||
-keep public class * implements java.lang.reflect.Type
|
||||
|
||||
-keep class com.vitorpamplona.amethyst.lnurl.** { *; }
|
||||
-keep class com.vitorpamplona.amethyst.model.** { *; }
|
||||
-keep class com.vitorpamplona.amethyst.service.** { *; }
|
||||
-keep class com.vitorpamplona.amethyst.ui.** { *; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * extends com.google.gson.TypeAdapter
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# Prevent R8 from leaving Data object members always null
|
||||
-keepclassmembers,allowobfuscation class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
||||
|
||||
@@ -668,7 +668,7 @@ class RichTextParserTest {
|
||||
fun testTextToParse() {
|
||||
val state = RichTextParser().parseText(textToParse, ImmutableListOfLists())
|
||||
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, ⚡41.7M, Nostr-Check.com, MR.Rabbit, Ancap.su, ⚡️satscoinsv@getalby.com, miceliomad@miceliomad.github.io/nostr/, zapper.lol, smies.me, baller.hodl",
|
||||
"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(", ")
|
||||
)
|
||||
|
||||
@@ -806,134 +806,101 @@ class RichTextParserTest {
|
||||
"HashTag(#bitcoin,)",
|
||||
"RegularText(1.7K,)",
|
||||
"RegularText(109)",
|
||||
"RegularText()",
|
||||
"HashTag(#concussion,)",
|
||||
"RegularText(1.1K,)",
|
||||
"RegularText(25)",
|
||||
"RegularText()",
|
||||
"HashTag(#press,)",
|
||||
"RegularText(0.9K,)",
|
||||
"RegularText(65)",
|
||||
"RegularText()",
|
||||
"HashTag(#france,)",
|
||||
"RegularText(492,)",
|
||||
"RegularText(46)",
|
||||
"RegularText()",
|
||||
"HashTag(#presse,)",
|
||||
"RegularText(480,)",
|
||||
"RegularText(42)",
|
||||
"RegularText()",
|
||||
"HashTag(#covid19,)",
|
||||
"RegularText(465,)",
|
||||
"RegularText(65)",
|
||||
"RegularText()",
|
||||
"HashTag(#nostr,)",
|
||||
"RegularText(414,)",
|
||||
"RegularText(109)",
|
||||
"RegularText()",
|
||||
"HashTag(#zapathon,)",
|
||||
"RegularText(386,)",
|
||||
"RegularText(76)",
|
||||
"RegularText()",
|
||||
"HashTag(#rssfeed,)",
|
||||
"RegularText(309,)",
|
||||
"RegularText(53)",
|
||||
"RegularText()",
|
||||
"HashTag(#btc,)",
|
||||
"RegularText(299,)",
|
||||
"RegularText(109)",
|
||||
"RegularText()",
|
||||
"HashTag(#news,)",
|
||||
"RegularText(294,)",
|
||||
"RegularText(91)",
|
||||
"RegularText()",
|
||||
"HashTag(#zap,)",
|
||||
"RegularText(283,)",
|
||||
"RegularText(109)",
|
||||
"RegularText()",
|
||||
"HashTag(#linux,)",
|
||||
"RegularText(253,)",
|
||||
"RegularText(88)",
|
||||
"RegularText()",
|
||||
"HashTag(#respond,)",
|
||||
"RegularText(246,)",
|
||||
"RegularText(90)",
|
||||
"RegularText()",
|
||||
"HashTag(#kompost,)",
|
||||
"RegularText(240,)",
|
||||
"RegularText(31)",
|
||||
"RegularText()",
|
||||
"HashTag(#plebchain,)",
|
||||
"RegularText(236,)",
|
||||
"RegularText(109)",
|
||||
"RegularText()",
|
||||
"HashTag(#gardenaward,)",
|
||||
"RegularText(236,)",
|
||||
"RegularText(31)",
|
||||
"RegularText()",
|
||||
"HashTag(#start,)",
|
||||
"RegularText(236,)",
|
||||
"RegularText(31)",
|
||||
"RegularText()",
|
||||
"HashTag(#unicef,)",
|
||||
"RegularText(233,)",
|
||||
"RegularText(32)",
|
||||
"RegularText()",
|
||||
"HashTag(#coronavirus,)",
|
||||
"RegularText(233,)",
|
||||
"RegularText(33)",
|
||||
"RegularText()",
|
||||
"HashTag(#bew,)",
|
||||
"RegularText(229,)",
|
||||
"RegularText(31)",
|
||||
"RegularText()",
|
||||
"HashTag(#balkon,)",
|
||||
"RegularText(229,)",
|
||||
"RegularText(31)",
|
||||
"RegularText()",
|
||||
"HashTag(#terrasse,)",
|
||||
"RegularText(229,)",
|
||||
"RegularText(31)",
|
||||
"RegularText()",
|
||||
"HashTag(#braininjuryawareness,)",
|
||||
"RegularText(229,)",
|
||||
"RegularText(24)",
|
||||
"RegularText()",
|
||||
"HashTag(#garten,)",
|
||||
"RegularText(220,)",
|
||||
"RegularText(21)",
|
||||
"RegularText()",
|
||||
"HashTag(#smart,)",
|
||||
"RegularText(220,)",
|
||||
"RegularText(21)",
|
||||
"RegularText()",
|
||||
"HashTag(#nsfw,)",
|
||||
"RegularText(211,)",
|
||||
"RegularText(85)",
|
||||
"RegularText()",
|
||||
"HashTag(#protoncalendar,)",
|
||||
"RegularText(206,)",
|
||||
"RegularText(31)",
|
||||
"RegularText()",
|
||||
"HashTag(#stacksats,)",
|
||||
"RegularText(195,)",
|
||||
"RegularText(99)",
|
||||
"RegularText()",
|
||||
"HashTag(#nokyc,)",
|
||||
"RegularText(179,)",
|
||||
"RegularText(98)",
|
||||
"RegularText()",
|
||||
"RegularText()",
|
||||
"RegularText(Emoji sentiment today)",
|
||||
"RegularText()",
|
||||
"RegularText(⚡ (1.6K) 👉 (1.4K) 🇪🇺 (1.2K) 🫂 (1.2K) 🇺🇸 (1.1K) 💜 (875) 🧠 (858) 😂 (830) 🔥 (690) 🤣 (566) 🤙 (525) ☕ (444) 👇 (443) 🙌🏻 (425) ☀ (307) 😎 (305) 🥳 (301) 🤔 (276) 🌻 (270) 🧡 (270) 🥇 (269) 🗓 (269) 🙏 (268) 🏆 (267) 🌱 (264) 📰 (230) 🏉 (221) 😭 (220) 💰 (219) 🔗 (209) 👀 (201) 😅 (199) ✨ (193) 🇷🇺 (182) 💪 (167) ✅ (164) 💤 (163) 🐶 (151) 🇨🇭 (141) 📝 (137) 😁 (136) 🌞 (136) 🐾 (136) ❤ (132) 💻 (126) 🚀 (125) 👍 (125) 🇧🇷 (125) 😊 (121) 📚 (120) ➡ (120) 👏 (118) 🎉 (117) 🎮 (115) 🤷 (113) 👋 (112) 💃 (108) 🕺🏻 (106) 💡 (104) 🚨 (99) 😆 (97) 💯 (95) ⚠ (92) 📢 (92) 🤗 (89) 😴 (87) 🔁 (83) 🐰 (81) 😀 (79) 🎟 (78) ⛏ (78) 🐦 (76) 💸 (76) ✌🏻 (75) 🤍 (73) 🇬🇧 (73) 🌽 (70) 🤡 (69) 🤮 (69) ❗ (66) 🤝 (65) 😉 (65) 🙇 (65) 🍻 (64) 🌍 (64) 💕 (63) 🌸 (62) 💬 (61) ☺ (61) 🇦🇷 (59) 🇮🇩 (57) 😳 (57) 😄 (57) 🎶 (57) 🥷🏻 (56) 🎵 (56) 😃 (56) 🔍 (55) 💥 (55) 🎲 (54) ✍ (54) 🕒 (53) ⬇ (53) 💙 (51) 🔒 (50) 📈 (50) 🪙 (50) 🌧 (50) 🥰 (50) 🕸 (50) 🌐 (50) 💭 (49) 🌙 (49) 😍 (49) 📱 (48) 🌟 (48) 🤩 (48) 💔 (47) 🔌 (47) 😋 (47) 🎖 (47) 🍣 (46) 📷 (46) 💼 (45) ⭐ (45) 🥔 (45) 🥺 (45) 👌 (44) 👷🏼 (43) 😱 (43) 📅 (43) 🤖 (43) 📸 (42) 📊 (42) 🦑 (40) 💵 (40) 🤦 (39) ❣ (38) 💎 (38) 🖤 (38) 📺 (37) 🇵🇱 (37) 🇯🇵 (36) 🔧 (36) 🤘 (36) 💖 (36) ‼ (35) 😢 (35) 😺 (34) 🔊 (34) 😏 (34) 🇸🇰 (34) 🏃 (34) 👩👧 (34) ⏰ (33) 👨💻 (33) 👑 (33) 👥 (32) 🖥 (32) 💨 (32) 💗 (31) 🇲🇽 (31) 📖 (31) 🚫 (31) 👊🏻 (31) 😡 (31) 🌎 (31) 👁 (30) 🗞 (30) 🍀 (30) 🍽 (29) 🐸 (29) 🥚 (29) 💩 (29) ✊🏾 (29) 😮 (29) 🌡 (29) 🙃 (28) 🔔 (28) 🇻🇪 (28) 💦 (28) 🎯 (28) 🎨 (28) 🍛 (28) 🖼 (27) ☝🏻 (27) 🛑 (27) 🙄 (27) 🧑🏻🤝🧑🏽 (27) 🌈 (27) 🥂 (26) 🇫🇮 (26) 🎥 (26) 😬 (26) 🥲 (25) 🦾 (24) 🤜 (24) 🙂 (24) 🖕 (24) 😩 (24) )",
|
||||
"RegularText()",
|
||||
"RegularText(Zap economy)",
|
||||
"RegularText()",
|
||||
"Link(⚡41.7M)",
|
||||
"RegularText(sats)",
|
||||
"RegularText((₿0.417))",
|
||||
"RegularText()",
|
||||
"RegularText(⚡41.7M sats (₿0.417) )",
|
||||
"RegularText(1,816 zappers & 920 zapped (unique pubkeys))",
|
||||
"RegularText(🌩️ 33,248 zaps, 1,253 sats per zap (avg))",
|
||||
"RegularText()",
|
||||
@@ -2248,7 +2215,7 @@ class RichTextParserTest {
|
||||
"HashTag(#214)",
|
||||
"RegularText(2%)",
|
||||
"RegularText(Satscoinsv,)",
|
||||
"Link(⚡️satscoinsv@getalby.com)",
|
||||
"RegularText(⚡️satscoinsv@getalby.com)",
|
||||
"RegularText(-)",
|
||||
"RegularText(80db64657ea0358c5332c5cca01565eeddd4b8799688b1c46d3cb2d7c966671f)",
|
||||
"HashTag(#215)",
|
||||
@@ -2458,7 +2425,7 @@ class RichTextParserTest {
|
||||
"HashTag(#249)",
|
||||
"RegularText(2%)",
|
||||
"RegularText(micmad,)",
|
||||
"Link(miceliomad@miceliomad.github.io/nostr/)",
|
||||
"RegularText(miceliomad@miceliomad.github.io/nostr/)",
|
||||
"RegularText(-)",
|
||||
"RegularText(cd806edcf8ff40ea94fa574ea9cd97da16e5beb2b85aac6e1d648b8388504343)",
|
||||
"HashTag(#250)",
|
||||
@@ -3805,7 +3772,7 @@ class RichTextParserTest {
|
||||
"HashTag(#471)",
|
||||
"RegularText(1%)",
|
||||
"RegularText(leonwankum,)",
|
||||
"SchemelessUrl(@leonawankum@BitcoinNostr.com)",
|
||||
"RegularText(@leonawankum@BitcoinNostr.com)",
|
||||
"RegularText()",
|
||||
"RegularText(-)",
|
||||
"RegularText(652d58acafa105af8475c0fe8029a52e7ddbc337b2bd9c98bb17a111dc4cde60)",
|
||||
@@ -4098,7 +4065,6 @@ https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db
|
||||
"RegularText(here:)",
|
||||
"Link(https://lnshort.it/live-stream-embeds/)",
|
||||
"RegularText()",
|
||||
"RegularText()",
|
||||
"Image(https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg)"
|
||||
)
|
||||
|
||||
@@ -4110,6 +4076,57 @@ https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNewLineAfterImage() {
|
||||
val text = "That’s it ! That’s the #note https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg "
|
||||
|
||||
val state = RichTextParser().parseText(text, ImmutableListOfLists())
|
||||
|
||||
printStateForDebug(state)
|
||||
|
||||
val expectedResult = listOf<String>(
|
||||
"RegularText(That’s)",
|
||||
"RegularText(it)",
|
||||
"RegularText(!)",
|
||||
"RegularText(That’s)",
|
||||
"RegularText(the)",
|
||||
"HashTag(#note)",
|
||||
"Image(https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg)"
|
||||
)
|
||||
|
||||
state.paragraphs.map { it.words }.flatten().forEachIndexed { index, seg ->
|
||||
Assert.assertEquals(
|
||||
expectedResult[index],
|
||||
"${seg.javaClass.simpleName.replace("Segment", "")}(${seg.segmentText})"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSapceAfterImage() {
|
||||
val text = "That’s it! https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg That’s the #note"
|
||||
|
||||
val state = RichTextParser().parseText(text, ImmutableListOfLists())
|
||||
|
||||
printStateForDebug(state)
|
||||
|
||||
val expectedResult = listOf<String>(
|
||||
"RegularText(That’s)",
|
||||
"RegularText(it!)",
|
||||
"Image(https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg)",
|
||||
"RegularText(That’s)",
|
||||
"RegularText(the)",
|
||||
"HashTag(#note)"
|
||||
)
|
||||
|
||||
state.paragraphs.map { it.words }.flatten().forEachIndexed { index, seg ->
|
||||
Assert.assertEquals(
|
||||
expectedResult[index],
|
||||
"${seg.javaClass.simpleName.replace("Segment", "")}(${seg.segmentText})"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun printStateForDebug(state: RichTextViewerState) {
|
||||
state.paragraphs.forEachIndexed { index, paragraph ->
|
||||
paragraph.words.forEach { seg ->
|
||||
|
||||
@@ -10,7 +10,9 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.Nip47URI
|
||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.model.Settings
|
||||
import com.vitorpamplona.amethyst.model.hexToByteArray
|
||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||
@@ -22,8 +24,6 @@ import com.vitorpamplona.amethyst.service.model.Event
|
||||
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.service.toNpub
|
||||
import com.vitorpamplona.amethyst.ui.actions.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.ui.note.Nip47URI
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
@@ -8,6 +8,8 @@ import coil.ImageLoader
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.disk.DiskCache
|
||||
import coil.util.DebugLogger
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
@@ -29,6 +31,7 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
|
||||
import java.io.File
|
||||
|
||||
object ServiceManager {
|
||||
private var account: Account? = null
|
||||
@@ -55,7 +58,8 @@ object ServiceManager {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
add(SvgDecoder.Factory())
|
||||
} // .logger(DebugLogger())
|
||||
}.logger(DebugLogger())
|
||||
.diskCache { SingletonDiskCache.get(context.applicationContext) }
|
||||
.okHttpClient { HttpClient.getHttpClient() }
|
||||
.respectCacheHeaders(false)
|
||||
.build()
|
||||
@@ -121,3 +125,28 @@ object ServiceManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SingletonDiskCache {
|
||||
|
||||
private const val DIRECTORY = "image_cache"
|
||||
private var instance: DiskCache? = null
|
||||
|
||||
@Synchronized
|
||||
fun get(context: Context): DiskCache {
|
||||
return instance ?: run {
|
||||
// Create the singleton disk cache instance.
|
||||
DiskCache.Builder()
|
||||
.directory(context.safeCacheDir.resolve(DIRECTORY))
|
||||
.maxSizePercent(0.2)
|
||||
.maximumMaxSizeBytes(500L * 1024 * 1024) // 250MB
|
||||
.build()
|
||||
.also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val Context.safeCacheDir: File
|
||||
get() {
|
||||
val cacheDir = checkNotNull(cacheDir) { "cacheDir == null" }
|
||||
return cacheDir.apply { mkdirs() }
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
|
||||
@UnstableApi object VideoCache {
|
||||
|
||||
@@ -25,7 +26,7 @@ import okhttp3.OkHttpClient
|
||||
exoDatabaseProvider = StandaloneDatabaseProvider(context)
|
||||
|
||||
simpleCache = SimpleCache(
|
||||
context.cacheDir,
|
||||
File(context.cacheDir, "exoplayer"),
|
||||
leastRecentlyUsedCacheEvictor,
|
||||
exoDatabaseProvider
|
||||
)
|
||||
|
||||
@@ -18,9 +18,7 @@ import com.vitorpamplona.amethyst.service.relays.Constants
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
import com.vitorpamplona.amethyst.ui.actions.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.note.Nip47URI
|
||||
import com.vitorpamplona.amethyst.ui.note.combineWith
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
@@ -1516,9 +1514,9 @@ class Account(
|
||||
|
||||
private fun isAcceptableDirect(note: Note): Boolean {
|
||||
if (!warnAboutPostsWithReports) {
|
||||
return note.reportsBy(userProfile()).isEmpty()
|
||||
return !note.hasReportsBy(userProfile())
|
||||
}
|
||||
return note.reportsBy(userProfile()).isEmpty() && // if user has not reported this post
|
||||
return !note.hasReportsBy(userProfile()) && // if user has not reported this post
|
||||
note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users
|
||||
}
|
||||
|
||||
|
||||
@@ -1086,7 +1086,7 @@ object LocalCache {
|
||||
}
|
||||
|
||||
try {
|
||||
val cachePath = File(Amethyst.instance.applicationContext.externalCacheDir, "NIP95")
|
||||
val cachePath = File(Amethyst.instance.applicationContext.cacheDir, "NIP95")
|
||||
cachePath.mkdirs()
|
||||
val file = File(cachePath, event.id)
|
||||
if (!file.exists()) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
data class Nip47URI(val pubKeyHex: HexKey, val relayUri: String?, val secret: HexKey?)
|
||||
@@ -51,20 +51,20 @@ open class Note(val idHex: String) {
|
||||
var replyTo: List<Note>? = null
|
||||
|
||||
// These fields are updated every time an event related to this note is received.
|
||||
var replies = setOf<Note>()
|
||||
var replies = listOf<Note>()
|
||||
private set
|
||||
var reactions = mapOf<String, Set<Note>>()
|
||||
var reactions = mapOf<String, List<Note>>()
|
||||
private set
|
||||
var boosts = setOf<Note>()
|
||||
var boosts = listOf<Note>()
|
||||
private set
|
||||
var reports = mapOf<User, Set<Note>>()
|
||||
var reports = mapOf<User, List<Note>>()
|
||||
private set
|
||||
var zaps = mapOf<Note, Note?>()
|
||||
private set
|
||||
var zapPayments = mapOf<Note, Note?>()
|
||||
private set
|
||||
|
||||
var relays = setOf<String>()
|
||||
var relays = listOf<String>()
|
||||
private set
|
||||
|
||||
var lastReactionsDownloadTime: Map<String, EOSETime> = emptyMap()
|
||||
@@ -151,15 +151,20 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun removeReply(note: Note) {
|
||||
replies = replies - note
|
||||
liveSet?.replies?.invalidateData()
|
||||
}
|
||||
fun removeBoost(note: Note) {
|
||||
boosts = boosts - note
|
||||
liveSet?.boosts?.invalidateData()
|
||||
if (note in replies) {
|
||||
replies = replies - note
|
||||
liveSet?.replies?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllChildNotes(): Set<Note> {
|
||||
fun removeBoost(note: Note) {
|
||||
if (note in boosts) {
|
||||
boosts = boosts - note
|
||||
liveSet?.boosts?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllChildNotes(): List<Note> {
|
||||
val toBeRemoved = replies +
|
||||
reactions.values.flatten() +
|
||||
boosts +
|
||||
@@ -169,13 +174,13 @@ open class Note(val idHex: String) {
|
||||
zapPayments.keys +
|
||||
zapPayments.values.filterNotNull()
|
||||
|
||||
replies = setOf<Note>()
|
||||
reactions = mapOf<String, Set<Note>>()
|
||||
boosts = setOf<Note>()
|
||||
reports = mapOf<User, Set<Note>>()
|
||||
replies = listOf<Note>()
|
||||
reactions = mapOf<String, List<Note>>()
|
||||
boosts = listOf<Note>()
|
||||
reports = mapOf<User, List<Note>>()
|
||||
zaps = mapOf<Note, Note?>()
|
||||
zapPayments = mapOf<Note, Note?>()
|
||||
relays = setOf<String>()
|
||||
relays = listOf<String>()
|
||||
lastReactionsDownloadTime = emptyMap()
|
||||
|
||||
liveSet?.replies?.invalidateData()
|
||||
@@ -193,14 +198,16 @@ open class Note(val idHex: String) {
|
||||
|
||||
if (reaction in reactions.keys && reactions[reaction]?.contains(note) == true) {
|
||||
reactions[reaction]?.let {
|
||||
val newList = it.minus(note)
|
||||
if (newList.isEmpty()) {
|
||||
reactions = reactions.minus(reaction)
|
||||
} else {
|
||||
reactions = reactions + Pair(reaction, newList)
|
||||
}
|
||||
if (note in it) {
|
||||
val newList = it.minus(note)
|
||||
if (newList.isEmpty()) {
|
||||
reactions = reactions.minus(reaction)
|
||||
} else {
|
||||
reactions = reactions + Pair(reaction, newList)
|
||||
}
|
||||
|
||||
liveSet?.reactions?.invalidateData()
|
||||
liveSet?.reactions?.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,7 +313,7 @@ open class Note(val idHex: String) {
|
||||
val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+"
|
||||
|
||||
if (reaction !in reactions.keys) {
|
||||
reactions = reactions + Pair(reaction, setOf(note))
|
||||
reactions = reactions + Pair(reaction, listOf(note))
|
||||
liveSet?.reactions?.invalidateData()
|
||||
} else if (reactions[reaction]?.contains(note) == false) {
|
||||
reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note)
|
||||
@@ -318,7 +325,7 @@ open class Note(val idHex: String) {
|
||||
val author = note.author ?: return
|
||||
|
||||
if (author !in reports.keys) {
|
||||
reports = reports + Pair(author, setOf(note))
|
||||
reports = reports + Pair(author, listOf(note))
|
||||
liveSet?.reports?.invalidateData()
|
||||
} else if (reports[author]?.contains(note) == false) {
|
||||
reports = reports + Pair(author, (reports[author] ?: emptySet()) + note)
|
||||
@@ -411,8 +418,8 @@ open class Note(val idHex: String) {
|
||||
return boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
|
||||
}
|
||||
|
||||
fun reportsBy(user: User): Set<Note> {
|
||||
return reports[user] ?: emptySet()
|
||||
fun hasReportsBy(user: User): Boolean {
|
||||
return reports[user]?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
fun reportAuthorsBy(users: Set<HexKey>): List<User> {
|
||||
@@ -576,9 +583,9 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
replyTo = null
|
||||
replies = emptySet()
|
||||
replies = emptyList()
|
||||
reactions = emptyMap()
|
||||
boosts = emptySet()
|
||||
boosts = emptyList()
|
||||
reports = emptyMap()
|
||||
zaps = emptyMap()
|
||||
}
|
||||
@@ -646,6 +653,8 @@ class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
|
||||
fun invalidateData() {
|
||||
if (!hasObservers()) return
|
||||
|
||||
checkNotInMainThread()
|
||||
|
||||
bundler.invalidate() {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
enum class ServersAvailable {
|
||||
// IMGUR,
|
||||
NOSTR_BUILD,
|
||||
NOSTRIMG,
|
||||
NOSTRFILES_DEV,
|
||||
NOSTRCHECK_ME,
|
||||
|
||||
// IMGUR_NIP_94,
|
||||
NOSTRIMG_NIP_94,
|
||||
NOSTR_BUILD_NIP_94,
|
||||
NOSTRFILES_DEV_NIP_94,
|
||||
NOSTRCHECK_ME_NIP_94,
|
||||
NIP95
|
||||
}
|
||||
@@ -2,33 +2,35 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.previews.BahaUrlPreview
|
||||
import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback
|
||||
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Stable
|
||||
object UrlCachedPreviewer {
|
||||
var cache = LruCache<String, UrlPreviewState>(100)
|
||||
private set
|
||||
|
||||
fun previewInfo(url: String, onReady: (UrlPreviewState) -> Unit) {
|
||||
checkNotInMainThread()
|
||||
|
||||
suspend fun previewInfo(
|
||||
url: String,
|
||||
onReady: suspend (UrlPreviewState) -> Unit
|
||||
) = withContext(Dispatchers.IO) {
|
||||
cache[url]?.let {
|
||||
onReady(it)
|
||||
return
|
||||
return@withContext
|
||||
}
|
||||
|
||||
BahaUrlPreview(
|
||||
url,
|
||||
object : IUrlPreviewCallback {
|
||||
override fun onComplete(urlInfo: UrlInfoItem) {
|
||||
override suspend fun onComplete(urlInfo: UrlInfoItem) = withContext(Dispatchers.IO) {
|
||||
cache[url]?.let {
|
||||
if (it is UrlPreviewState.Loaded || it is UrlPreviewState.Empty) {
|
||||
onReady(it)
|
||||
return
|
||||
return@withContext
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +44,10 @@ object UrlCachedPreviewer {
|
||||
onReady(state)
|
||||
}
|
||||
|
||||
override fun onFailed(throwable: Throwable) {
|
||||
override suspend fun onFailed(throwable: Throwable) = withContext(Dispatchers.IO) {
|
||||
cache[url]?.let {
|
||||
onReady(it)
|
||||
return
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val state = UrlPreviewState.Error(throwable.message ?: "Error Loading url preview")
|
||||
|
||||
@@ -475,6 +475,7 @@ class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
|
||||
fun invalidateData() {
|
||||
if (!hasObservers()) return
|
||||
checkNotInMainThread()
|
||||
|
||||
bundler.invalidate() {
|
||||
|
||||
@@ -54,7 +54,7 @@ object CachedRichTextParser {
|
||||
|
||||
// Android9 seems to have an issue starting this regex.
|
||||
val noProtocolUrlValidator = try {
|
||||
Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]*[^\\p{IsHan}\\p{IsHiragana}\\p{IsKatakana}])*\\/?)(.*)")
|
||||
Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+[^\\p{IsHan}\\p{IsHiragana}\\p{IsKatakana}])*\\/?)(.*)")
|
||||
} catch (e: Exception) {
|
||||
Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)")
|
||||
}
|
||||
@@ -119,7 +119,7 @@ class RichTextParser() {
|
||||
|
||||
val isRTL = isArabic(paragraph)
|
||||
|
||||
val wordList = paragraph.split(' ')
|
||||
val wordList = paragraph.trimEnd().split(' ')
|
||||
wordList.forEach { word ->
|
||||
val wordSegment = wordIdentifier(word, images, urls, emojis, tags)
|
||||
if (wordSegment !is RegularTextSegment) {
|
||||
@@ -182,8 +182,8 @@ class RichTextParser() {
|
||||
} else if (schemelessMatcher.find()) {
|
||||
val url = schemelessMatcher.group(1) // url
|
||||
val additionalChars = schemelessMatcher.group(4) // additional chars
|
||||
val pattern = "^([A-Za-z0-9-]+(\\.[A-Za-z0-9]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?".toRegex(RegexOption.IGNORE_CASE)
|
||||
if (pattern.matches(word)) {
|
||||
val pattern = "^([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?".toRegex(RegexOption.IGNORE_CASE)
|
||||
if (pattern.find(word) != null) {
|
||||
SchemelessUrlSegment(word, url, additionalChars)
|
||||
} else {
|
||||
RegularTextSegment(word)
|
||||
|
||||
@@ -63,7 +63,7 @@ class LocationUtil(context: Context) {
|
||||
}
|
||||
|
||||
class ReverseGeoLocationUtil {
|
||||
fun execute(
|
||||
suspend fun execute(
|
||||
location: Location,
|
||||
context: Context
|
||||
): String? {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.net.Uri
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.Nip47URI
|
||||
import com.vitorpamplona.amethyst.model.decodePublicKey
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
|
||||
data class Nip47URI(val pubKeyHex: HexKey, val relayUri: String?, val secret: HexKey?)
|
||||
|
||||
// Rename to the corect nip number when ready.
|
||||
object Nip47 {
|
||||
fun parse(uri: String): Nip47URI {
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.CryptoUtils
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.service.relays.bytesUsedInMemory
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import java.lang.reflect.Type
|
||||
import java.math.BigDecimal
|
||||
@@ -24,6 +25,16 @@ open class Event(
|
||||
val content: String,
|
||||
val sig: HexKey
|
||||
) : EventInterface {
|
||||
|
||||
override fun countMemory(): Long {
|
||||
return 12L +
|
||||
id.bytesUsedInMemory() +
|
||||
pubKey.bytesUsedInMemory() +
|
||||
tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } +
|
||||
content.bytesUsedInMemory() +
|
||||
sig.bytesUsedInMemory()
|
||||
}
|
||||
|
||||
override fun id(): HexKey = id
|
||||
|
||||
override fun pubKey(): HexKey = pubKey
|
||||
|
||||
@@ -6,6 +6,8 @@ import java.math.BigDecimal
|
||||
|
||||
@Immutable
|
||||
interface EventInterface {
|
||||
fun countMemory(): Long
|
||||
|
||||
fun id(): HexKey
|
||||
|
||||
fun pubKey(): HexKey
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
import android.net.Uri
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
|
||||
class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
private val imageExtensionArray = arrayOf(".gif", ".png", ".jpg", ".jpeg", ".bmp", ".webp")
|
||||
|
||||
fun fetchUrlPreview(timeOut: Int = 30000) {
|
||||
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
|
||||
callback?.onFailed(throwable)
|
||||
}
|
||||
scope.launch(exceptionHandler) {
|
||||
suspend fun fetchUrlPreview(timeOut: Int = 30000) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
fetch(timeOut)
|
||||
} catch (t: Throwable) {
|
||||
callback?.onFailed(t)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetch(timeOut: Int = 30000) {
|
||||
checkNotInMainThread()
|
||||
lateinit var urlInfoItem: UrlInfoItem
|
||||
if (checkIsImageUrl()) {
|
||||
urlInfoItem = UrlInfoItem(url = url, image = url)
|
||||
@@ -43,7 +39,6 @@ class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) {
|
||||
}
|
||||
|
||||
fun cleanUp() {
|
||||
scope.cancel()
|
||||
callback = null
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
interface IUrlPreviewCallback {
|
||||
fun onComplete(urlInfo: UrlInfoItem)
|
||||
fun onFailed(throwable: Throwable)
|
||||
suspend fun onComplete(urlInfo: UrlInfoItem)
|
||||
suspend fun onFailed(throwable: Throwable)
|
||||
}
|
||||
|
||||
@@ -56,9 +56,6 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val uri = intent?.data?.toString()
|
||||
val startingPage = uriToRoute(uri)
|
||||
|
||||
LocalPreferences.migrateSingleUserPrefs()
|
||||
val language = LocalPreferences.getPreferredLanguage()
|
||||
if (language.isNotBlank()) {
|
||||
@@ -138,12 +135,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
println("AAA -OnNew Intent")
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
// network is available for use
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.core.net.toFile
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import okhttp3.*
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -189,17 +190,20 @@ class ImgurServer : FileServer() {
|
||||
}
|
||||
|
||||
class NostrBuildServer : FileServer() {
|
||||
override fun postUrl(contentType: String?) = "https://nostr.build/api/upload/android.php"
|
||||
override fun postUrl(contentType: String?) = "https://nostr.build/api/v2/upload/files"
|
||||
override fun parseUrlFromSuccess(body: String): String? {
|
||||
val url = jacksonObjectMapper().readTree(body) // return url.toString()
|
||||
val tree = jacksonObjectMapper().readTree(body)
|
||||
val data = tree?.get("data")
|
||||
val data0 = data?.get(0)
|
||||
val url = data0?.get("url")
|
||||
return url.toString().replace("\"", "")
|
||||
}
|
||||
|
||||
override fun inputParameterName(contentType: String?): String {
|
||||
return "fileToUpload"
|
||||
return "file"
|
||||
}
|
||||
|
||||
override fun clientID(info: String) = null
|
||||
override fun clientID(info: String) = ImageUploader.NIP98Header("https://nostr.build/api/v2/upload/files", "POST", info)
|
||||
}
|
||||
|
||||
class NostrFilesDevServer : FileServer() {
|
||||
|
||||
@@ -7,14 +7,15 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -28,8 +29,6 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, channel: PublicChatChannel? = null) {
|
||||
@@ -44,7 +43,7 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
|
||||
) {
|
||||
Surface() {
|
||||
Column(
|
||||
modifier = Modifier.padding(10.dp)
|
||||
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -57,14 +56,10 @@ fun NewChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, chan
|
||||
onClose()
|
||||
})
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
PostButton(
|
||||
onPost = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postViewModel.create()
|
||||
onClose()
|
||||
}
|
||||
postViewModel.create()
|
||||
onClose()
|
||||
},
|
||||
postViewModel.channelName.value.text.isNotBlank()
|
||||
)
|
||||
|
||||
@@ -3,8 +3,11 @@ package com.vitorpamplona.amethyst.ui.actions
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NewChannelViewModel : ViewModel() {
|
||||
private var account: Account? = null
|
||||
@@ -25,24 +28,26 @@ class NewChannelViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun create() {
|
||||
this.account?.let { account ->
|
||||
if (originalChannel == null) {
|
||||
account.sendCreateNewChannel(
|
||||
channelName.value.text,
|
||||
channelDescription.value.text,
|
||||
channelPicture.value.text
|
||||
)
|
||||
} else {
|
||||
account.sendChangeChannel(
|
||||
channelName.value.text,
|
||||
channelDescription.value.text,
|
||||
channelPicture.value.text,
|
||||
originalChannel!!
|
||||
)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account?.let { account ->
|
||||
if (originalChannel == null) {
|
||||
account.sendCreateNewChannel(
|
||||
channelName.value.text,
|
||||
channelDescription.value.text,
|
||||
channelPicture.value.text
|
||||
)
|
||||
} else {
|
||||
account.sendChangeChannel(
|
||||
channelName.value.text,
|
||||
channelDescription.value.text,
|
||||
channelPicture.value.text,
|
||||
originalChannel!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear()
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
|
||||
@@ -45,6 +45,7 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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
|
||||
|
||||
@@ -76,6 +76,7 @@ import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
||||
@@ -94,6 +95,7 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
@@ -736,16 +738,32 @@ fun DisplayLocationObserver(geoLocation: Flow<String>) {
|
||||
@Composable
|
||||
fun DisplayLocationInTitle(geohash: String) {
|
||||
val context = LocalContext.current
|
||||
val cityName = remember(geohash) {
|
||||
ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)
|
||||
|
||||
var cityName by remember(geohash) {
|
||||
mutableStateOf<String>(geohash)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = cityName ?: geohash,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier.padding(start = Size5dp)
|
||||
)
|
||||
LaunchedEffect(key1 = geohash) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newCityName = ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)?.ifBlank { null }
|
||||
|
||||
if (newCityName != null && newCityName != cityName) {
|
||||
cityName = newCityName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (geohash != "s0000") {
|
||||
Text(
|
||||
text = cityName,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier.padding(start = Size5dp)
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LoadingAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@@ -1099,21 +1117,6 @@ fun CreateButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier
|
||||
}
|
||||
}
|
||||
|
||||
enum class ServersAvailable {
|
||||
// IMGUR,
|
||||
NOSTR_BUILD,
|
||||
NOSTRIMG,
|
||||
NOSTRFILES_DEV,
|
||||
NOSTRCHECK_ME,
|
||||
|
||||
// IMGUR_NIP_94,
|
||||
NOSTRIMG_NIP_94,
|
||||
NOSTR_BUILD_NIP_94,
|
||||
NOSTRFILES_DEV_NIP_94,
|
||||
NOSTRCHECK_ME_NIP_94,
|
||||
NIP95
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageVideoDescription(
|
||||
uri: Uri,
|
||||
|
||||
@@ -13,6 +13,7 @@ import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.Options
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.CryptoUtils
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import okio.Buffer
|
||||
@@ -36,7 +37,7 @@ private fun svgString(msg: String): String {
|
||||
checkNotInMainThread()
|
||||
|
||||
val hash = CryptoUtils.sha256(msg.toByteArray())
|
||||
val hashHex = hash.joinToString(separator = "") { b -> "%02x".format(b) }
|
||||
val hashHex = hash.toHexKey()
|
||||
val bgColor = bytesToRGB(hash[0], hash[1], hash[2])
|
||||
val fgColor = bytesToRGB(hash[3], hash[4], hash[5])
|
||||
val bodyIndex = byteMod10(hash[6])
|
||||
@@ -94,6 +95,7 @@ object Robohash {
|
||||
.Builder(context)
|
||||
.data("robohash:$message")
|
||||
.fetcherFactory(HashImageFetcher.Factory)
|
||||
.addHeader("Cache-Control", "max-age=31536000")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -64,19 +65,30 @@ fun RobohashFallbackAsyncImage(
|
||||
model = Robohash.imageRequest(context, robot)
|
||||
)
|
||||
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
placeholder = painter,
|
||||
fallback = painter,
|
||||
error = painter,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality
|
||||
)
|
||||
if (model != null) {
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
placeholder = painter,
|
||||
fallback = painter,
|
||||
error = painter,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
colorFilter = colorFilter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -30,7 +30,7 @@ fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel)
|
||||
} else {
|
||||
var urlPreviewState by remember(url) {
|
||||
mutableStateOf(
|
||||
UrlCachedPreviewer.cache.get(url)?.let { it } ?: UrlPreviewState.Loading
|
||||
UrlCachedPreviewer.cache.get(url) ?: UrlPreviewState.Loading
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@ fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel)
|
||||
LaunchedEffect(url) {
|
||||
launch(Dispatchers.IO) {
|
||||
UrlCachedPreviewer.previewInfo(url) {
|
||||
launch(Dispatchers.Main) {
|
||||
urlPreviewState = it
|
||||
}
|
||||
urlPreviewState = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ fun AppNavigation(
|
||||
|
||||
navController: NavHostController,
|
||||
accountViewModel: AccountViewModel,
|
||||
startingPage: String? = null,
|
||||
themeViewModel: ThemeViewModel
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -250,7 +249,10 @@ fun AppNavigation(
|
||||
}
|
||||
}
|
||||
|
||||
var actionableNextPage by remember { mutableStateOf(startingPage) }
|
||||
val activity = LocalContext.current.getActivity()
|
||||
var actionableNextPage by remember {
|
||||
mutableStateOf(uriToRoute(activity.intent?.data?.toString()?.ifBlank { null }))
|
||||
}
|
||||
actionableNextPage?.let {
|
||||
LaunchedEffect(it) {
|
||||
navController.navigate(it) {
|
||||
@@ -261,7 +263,6 @@ fun AppNavigation(
|
||||
actionableNextPage = null
|
||||
}
|
||||
|
||||
val activity = LocalContext.current.getActivity()
|
||||
DisposableEffect(navController) {
|
||||
val consumer = Consumer<Intent> { intent ->
|
||||
val uri = intent?.data?.toString()
|
||||
|
||||
@@ -11,11 +11,8 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
@@ -39,8 +36,8 @@ 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
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -78,11 +75,20 @@ import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.note.CommunityHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -109,29 +115,77 @@ fun AppTopBar(
|
||||
}
|
||||
}
|
||||
|
||||
RenderTopRouteBar(currentRoute, followLists, scaffoldState, accountViewModel, nav)
|
||||
val id by remember(navEntryState.value) {
|
||||
derivedStateOf {
|
||||
navEntryState.value?.arguments?.getString("id")
|
||||
}
|
||||
}
|
||||
|
||||
RenderTopRouteBar(currentRoute, id, followLists, scaffoldState, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderTopRouteBar(
|
||||
currentRoute: String?,
|
||||
id: String?,
|
||||
followLists: FollowListViewModel,
|
||||
scaffoldState: ScaffoldState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (currentRoute) {
|
||||
Route.Channel.base -> NoTopBar()
|
||||
Route.Room.base -> NoTopBar()
|
||||
Route.Community.base -> NoTopBar()
|
||||
Route.Hashtag.base -> NoTopBar()
|
||||
Route.Geohash.base -> NoTopBar()
|
||||
// Route.Profile.route -> TopBarWithBackButton(nav)
|
||||
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Discover.base -> DiscoveryTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||
else -> MainTopBar(scaffoldState, accountViewModel, nav)
|
||||
else -> {
|
||||
if (id != null) {
|
||||
when (currentRoute) {
|
||||
Route.Channel.base -> LoadChannel(baseChannelHex = id) {
|
||||
ChannelHeader(
|
||||
baseChannel = it,
|
||||
showVideo = true,
|
||||
showBottomDiviser = true,
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 11.dp),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
Route.Room.base -> LoadUser(baseUserHex = id) {
|
||||
if (it != null) {
|
||||
ChatroomHeader(
|
||||
baseUser = it,
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 11.dp),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
} else {
|
||||
Spacer(BottomTopHeight)
|
||||
}
|
||||
}
|
||||
Route.Community.base -> LoadAddressableNote(aTagHex = id) {
|
||||
if (it != null) {
|
||||
CommunityHeader(
|
||||
baseNote = it,
|
||||
showBottomDiviser = true,
|
||||
sendToCommunity = false,
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 10.dp),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
} else {
|
||||
Spacer(BottomTopHeight)
|
||||
}
|
||||
}
|
||||
Route.Hashtag.base -> HashtagHeader(id, Modifier.padding(vertical = 0.dp, horizontal = 10.dp), accountViewModel)
|
||||
Route.Geohash.base -> GeoHashHeader(id, Modifier.padding(vertical = 0.dp, horizontal = 10.dp), accountViewModel)
|
||||
else -> MainTopBar(scaffoldState, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
MainTopBar(scaffoldState, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,17 +333,14 @@ private fun LoggedInUserPictureDrawer(
|
||||
val profilePicture = remember(accountUserState) { accountUserState?.user?.profilePicture() }
|
||||
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
onClick = onClick
|
||||
) {
|
||||
RobohashAsyncImageProxy(
|
||||
robot = pubkeyHex,
|
||||
model = profilePicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.width(34.dp)
|
||||
.height(34.dp)
|
||||
.clip(shape = CircleShape)
|
||||
modifier = HeaderPictureModifier,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -514,6 +565,8 @@ fun debugState(context: Context) {
|
||||
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size)
|
||||
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
|
||||
|
||||
Log.d("STATE DUMP", "Memory used by Events: " + LocalCache.notes.values.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + " MB")
|
||||
|
||||
LocalCache.notes.values.groupBy { it.event?.kind() }.forEach {
|
||||
Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value.size} elements ")
|
||||
}
|
||||
|
||||
@@ -33,12 +33,10 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -58,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.theme.ChatBubbleMaxSizeModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeMe
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeThem
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
@@ -68,6 +67,8 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -254,9 +255,11 @@ fun NormalChatNote(
|
||||
|
||||
if (routeForLastRead != null) {
|
||||
LaunchedEffect(key1 = routeForLastRead) {
|
||||
val createdAt = note.createdAt()
|
||||
if (createdAt != null) {
|
||||
accountViewModel.account.markAsRead(routeForLastRead, createdAt)
|
||||
launch(Dispatchers.IO) {
|
||||
val createdAt = note.createdAt()
|
||||
if (createdAt != null) {
|
||||
accountViewModel.account.markAsRead(routeForLastRead, createdAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -573,18 +576,18 @@ private fun StatusRow(
|
||||
|
||||
@Composable
|
||||
fun ChatTimeAgo(baseNote: Note) {
|
||||
val context = LocalContext.current
|
||||
val nowStr = stringResource(id = R.string.now)
|
||||
|
||||
val timeStr by remember(baseNote) {
|
||||
val time by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
timeAgoShort(baseNote.createdAt() ?: 0, context = context)
|
||||
timeAgoShort(baseNote.createdAt() ?: 0, nowStr)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = timeStr,
|
||||
text = time,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
fontSize = 12.sp,
|
||||
fontSize = Font12SP,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
val newItemColor = MaterialTheme.colors.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = messageSetCard) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
val isNew = messageSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
accountViewModel.account.markAsRead(routeForLastRead, messageSetCard.createdAt())
|
||||
|
||||
@@ -162,7 +162,6 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
private fun Galeries(
|
||||
multiSetCard: MultiSetCard,
|
||||
@@ -259,7 +258,9 @@ fun RenderZapGallery(
|
||||
) {
|
||||
ZappedIcon(
|
||||
modifier = remember {
|
||||
Modifier.size(Size25dp).align(Alignment.TopEnd)
|
||||
Modifier
|
||||
.size(Size25dp)
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -281,7 +282,11 @@ fun RenderBoostGallery(
|
||||
modifier = NotificationIconModifierSmaller
|
||||
) {
|
||||
RepostedIcon(
|
||||
modifier = remember { Modifier.size(Size19dp).align(Alignment.TopEnd) }
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(Size19dp)
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -344,20 +349,9 @@ private fun ParseAuthorCommentAndAmount(
|
||||
|
||||
LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
(zapRequest.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = accountViewModel.decryptZap(zapRequest)
|
||||
val amount = (zapEvent?.event as? LnZapEvent)?.amount
|
||||
if (decryptedContent != null) {
|
||||
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
val newState = ZapAmountCommentNotification(newAuthor, decryptedContent.content.ifBlank { null }, showAmountAxis(amount))
|
||||
|
||||
launch(Dispatchers.Main) { content.value = newState }
|
||||
} else {
|
||||
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
|
||||
val newState = ZapAmountCommentNotification(zapRequest.author, zapRequest.event?.content()?.ifBlank { null }, showAmountAxis(amount))
|
||||
launch(Dispatchers.Main) { content.value = newState }
|
||||
}
|
||||
}
|
||||
val newState = loadAmountState(zapRequest, zapEvent, accountViewModel)
|
||||
if (newState != null) {
|
||||
content.value = newState
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,6 +359,34 @@ private fun ParseAuthorCommentAndAmount(
|
||||
onReady(content)
|
||||
}
|
||||
|
||||
private suspend fun loadAmountState(
|
||||
zapRequest: Note,
|
||||
zapEvent: Note?,
|
||||
accountViewModel: AccountViewModel
|
||||
): ZapAmountCommentNotification? {
|
||||
(zapRequest.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = accountViewModel.decryptZap(zapRequest)
|
||||
val amount = (zapEvent?.event as? LnZapEvent)?.amount
|
||||
if (decryptedContent != null) {
|
||||
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
return ZapAmountCommentNotification(
|
||||
newAuthor,
|
||||
decryptedContent.content.ifBlank { null },
|
||||
showAmountAxis(amount)
|
||||
)
|
||||
} else {
|
||||
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
|
||||
return ZapAmountCommentNotification(
|
||||
zapRequest.author,
|
||||
zapRequest.event?.content()?.ifBlank { null },
|
||||
showAmountAxis(amount)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderState(
|
||||
content: MutableState<ZapAmountCommentNotification>,
|
||||
|
||||
@@ -160,6 +160,7 @@ import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
@@ -172,6 +173,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
@@ -511,29 +513,32 @@ fun CommunityHeader(
|
||||
) {
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = modifier.clickable {
|
||||
if (sendToCommunity) {
|
||||
routeFor(baseNote, accountViewModel.userProfile())?.let {
|
||||
nav(it)
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = modifier.clickable {
|
||||
if (sendToCommunity) {
|
||||
routeFor(baseNote, accountViewModel.userProfile())?.let {
|
||||
nav(it)
|
||||
}
|
||||
} else {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
} else {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
) {
|
||||
ShortCommunityHeader(baseNote, expanded, accountViewModel, nav)
|
||||
|
||||
if (expanded.value) {
|
||||
LongCommunityHeader(baseNote, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
) {
|
||||
ShortCommunityHeader(baseNote, expanded, accountViewModel, nav)
|
||||
|
||||
if (expanded.value) {
|
||||
LongCommunityHeader(baseNote, accountViewModel, nav)
|
||||
if (showBottomDiviser) {
|
||||
Divider(
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showBottomDiviser) {
|
||||
Divider(
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -690,11 +695,7 @@ fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState<Boole
|
||||
model = it,
|
||||
contentDescription = stringResource(R.string.profile_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.width(Size35dp)
|
||||
.height(Size35dp)
|
||||
.clip(shape = CircleShape)
|
||||
modifier = HeaderPictureModifier
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1949,6 +1950,26 @@ fun RenderPostApproval(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadAddressableNote(aTagHex: String, content: @Composable (AddressableNote?) -> Unit) {
|
||||
var note by remember(aTagHex) {
|
||||
mutableStateOf<AddressableNote?>(LocalCache.getAddressableNoteIfExists(aTagHex))
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
LaunchedEffect(key1 = aTagHex) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newNote = LocalCache.checkGetOrCreateAddressableNote(aTagHex)
|
||||
if (newNote != note) {
|
||||
note = newNote
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content(note)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadAddressableNote(aTag: ATag, content: @Composable (AddressableNote?) -> Unit) {
|
||||
var note by remember(aTag) {
|
||||
@@ -1958,7 +1979,10 @@ fun LoadAddressableNote(aTag: ATag, content: @Composable (AddressableNote?) -> U
|
||||
if (note == null) {
|
||||
LaunchedEffect(key1 = aTag) {
|
||||
launch(Dispatchers.IO) {
|
||||
note = LocalCache.getOrCreateAddressableNote(aTag)
|
||||
val newNote = LocalCache.getOrCreateAddressableNote(aTag)
|
||||
if (newNote != note) {
|
||||
note = newNote
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2427,12 +2451,21 @@ fun SecondUserInfoRow(
|
||||
@Composable
|
||||
fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val cityName = remember(geohash) {
|
||||
ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)
|
||||
var cityName by remember(geohash) {
|
||||
mutableStateOf<String>(geohash)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = geohash) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newCityName = ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)?.ifBlank { null }
|
||||
if (newCityName != null && newCityName != cityName) {
|
||||
cityName = newCityName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(cityName ?: geohash),
|
||||
text = AnnotatedString(cityName),
|
||||
onClick = { nav("Geohash/$geohash") },
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colors.primary.copy(
|
||||
@@ -3222,7 +3255,7 @@ private fun ObserverAndRenderNIP95(
|
||||
// Creates a new object when the event arrives to force an update of the image.
|
||||
val note = noteState?.note
|
||||
val uri = header.toNostrUri()
|
||||
val localDir = note?.idHex?.let { File(File(appContext.externalCacheDir, "NIP95"), it) }
|
||||
val localDir = note?.idHex?.let { File(File(appContext.cacheDir, "NIP95"), it) }
|
||||
val blurHash = eventHeader.blurhash()
|
||||
val dimensions = eventHeader.dimensions()
|
||||
val description = eventHeader.content
|
||||
@@ -3505,6 +3538,7 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = Size5dp)
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
|
||||
@@ -112,6 +112,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
@@ -411,65 +412,97 @@ private fun ReactionDetailGallery(
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val zapsState by baseNote.live().zaps.observeAsState()
|
||||
val boostsState by baseNote.live().boosts.observeAsState()
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
|
||||
val defaultBackgroundColor = MaterialTheme.colors.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
|
||||
val hasReactions by remember(zapsState, boostsState, reactionsState) {
|
||||
derivedStateOf {
|
||||
baseNote.zaps.isNotEmpty() ||
|
||||
baseNote.boosts.isNotEmpty() ||
|
||||
baseNote.reactions.isNotEmpty()
|
||||
}
|
||||
}
|
||||
val hasReactions by baseNote.live().zaps.combineWith(
|
||||
baseNote.live().boosts,
|
||||
baseNote.live().reactions
|
||||
) { zapState, boostState, reactionState ->
|
||||
zapState?.note?.zaps?.isNotEmpty() ?: false ||
|
||||
boostState?.note?.boosts?.isNotEmpty() ?: false ||
|
||||
reactionState?.note?.reactions?.isNotEmpty() ?: false
|
||||
}.distinctUntilChanged().observeAsState(
|
||||
baseNote.zaps.isNotEmpty() || baseNote.boosts.isNotEmpty() || baseNote.reactions.isNotEmpty()
|
||||
)
|
||||
|
||||
if (hasReactions) {
|
||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.padding(start = 10.dp, top = 5.dp)) {
|
||||
Column() {
|
||||
val zapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.mapNotNull { it.value?.let { zapEvent -> CombinedZap(it.key, zapEvent) } }.toImmutableList() } }
|
||||
val boostEvents by remember(boostsState) { derivedStateOf { baseNote.boosts.toImmutableList() } }
|
||||
val likeEvents by remember(reactionsState) { derivedStateOf { baseNote.reactions.toImmutableMap() } }
|
||||
|
||||
val hasZapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.isNotEmpty() } }
|
||||
val hasBoostEvents by remember(boostsState) { derivedStateOf { baseNote.boosts.isNotEmpty() } }
|
||||
val hasLikeEvents by remember(reactionsState) { derivedStateOf { baseNote.reactions.isNotEmpty() } }
|
||||
|
||||
if (hasZapEvents) {
|
||||
RenderZapGallery(
|
||||
zapEvents,
|
||||
backgroundColor,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
|
||||
if (hasBoostEvents) {
|
||||
RenderBoostGallery(
|
||||
boostEvents,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
|
||||
if (hasLikeEvents) {
|
||||
likeEvents.forEach {
|
||||
val reactions = remember(it.value) { it.value.toImmutableList() }
|
||||
RenderLikeGallery(
|
||||
it.key,
|
||||
reactions,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
WatchZapAndRenderGallery(baseNote, backgroundColor, nav, accountViewModel)
|
||||
WatchBoostsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||
WatchReactionsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchBoostsAndRenderGallery(
|
||||
baseNote: Note,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val boostsState by baseNote.live().boosts.observeAsState()
|
||||
val boostsEvents by remember(boostsState) {
|
||||
derivedStateOf { baseNote.boosts.toImmutableList() }
|
||||
}
|
||||
|
||||
if (boostsEvents.isNotEmpty()) {
|
||||
RenderBoostGallery(
|
||||
boostsEvents,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchReactionsAndRenderGallery(
|
||||
baseNote: Note,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||
val reactionEvents by remember(reactionsState) {
|
||||
derivedStateOf { baseNote.reactions.toImmutableMap() }
|
||||
}
|
||||
|
||||
if (reactionEvents.isNotEmpty()) {
|
||||
reactionEvents.forEach {
|
||||
val reactions = remember(it.value) { it.value.toImmutableList() }
|
||||
RenderLikeGallery(
|
||||
it.key,
|
||||
reactions,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchZapAndRenderGallery(
|
||||
baseNote: Note,
|
||||
backgroundColor: MutableState<Color>,
|
||||
nav: (String) -> Unit,
|
||||
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() }
|
||||
}
|
||||
|
||||
if (zapEvents.isNotEmpty()) {
|
||||
RenderZapGallery(
|
||||
zapEvents,
|
||||
backgroundColor,
|
||||
nav,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoostWithDialog(
|
||||
baseNote: Note,
|
||||
@@ -1456,14 +1489,19 @@ val OneGiga = BigDecimal(1000000000)
|
||||
val OneMega = BigDecimal(1000000)
|
||||
val OneKilo = BigDecimal(1000)
|
||||
|
||||
var dfG: DecimalFormat = DecimalFormat("#.0G")
|
||||
var dfM: DecimalFormat = DecimalFormat("#.0M")
|
||||
var dfK: DecimalFormat = DecimalFormat("#.0k")
|
||||
var dfN: DecimalFormat = DecimalFormat("#")
|
||||
|
||||
fun showAmount(amount: BigDecimal?): String {
|
||||
if (amount == null) return ""
|
||||
if (amount.abs() < BigDecimal(0.01)) return ""
|
||||
|
||||
return when {
|
||||
amount >= OneGiga -> "%.1fG".format(amount.div(OneGiga).setScale(1, RoundingMode.HALF_UP))
|
||||
amount >= OneMega -> "%.1fM".format(amount.div(OneMega).setScale(1, RoundingMode.HALF_UP))
|
||||
amount >= OneKilo -> "%.1fk".format(amount.div(OneKilo).setScale(1, RoundingMode.HALF_UP))
|
||||
else -> "%.0f".format(amount)
|
||||
amount >= OneGiga -> dfG.format(amount.div(OneGiga).setScale(1, RoundingMode.HALF_UP))
|
||||
amount >= OneMega -> dfM.format(amount.div(OneMega).setScale(1, RoundingMode.HALF_UP))
|
||||
amount >= OneKilo -> dfK.format(amount.div(OneKilo).setScale(1, RoundingMode.HALF_UP))
|
||||
else -> dfN.format(amount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ fun timeAgo(mills: Long?, context: Context): String {
|
||||
.replace("Yesterday", "1" + context.getString(R.string.d))
|
||||
}
|
||||
|
||||
fun timeAgoShort(mills: Long?, context: Context): String {
|
||||
fun timeAgoShort(mills: Long?, stringForNow: String): String {
|
||||
if (mills == null) return " "
|
||||
|
||||
var humanReadable = DateUtils.getRelativeTimeSpanString(
|
||||
@@ -37,7 +37,7 @@ fun timeAgoShort(mills: Long?, context: Context): String {
|
||||
DateUtils.FORMAT_ABBREV_ALL
|
||||
).toString()
|
||||
if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) {
|
||||
humanReadable = context.getString(R.string.now)
|
||||
humanReadable = stringForNow
|
||||
}
|
||||
|
||||
return humanReadable
|
||||
|
||||
@@ -67,6 +67,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Nip47URI
|
||||
import com.vitorpamplona.amethyst.model.decodePublicKey
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
@@ -11,6 +12,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
@@ -86,6 +88,7 @@ fun ChatroomFeedLoaded(
|
||||
top = 10.dp,
|
||||
bottom = 10.dp
|
||||
),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
reverseLayout = true,
|
||||
state = listState
|
||||
) {
|
||||
|
||||
@@ -292,10 +292,12 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -329,6 +329,11 @@ fun NoteMaster(
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ObserveDisplayNip05Status(baseNote, remember { Modifier.weight(1f) })
|
||||
|
||||
val geo = remember { noteEvent.getGeoHash() }
|
||||
if (geo != null) {
|
||||
DisplayLocation(geo, nav)
|
||||
}
|
||||
|
||||
val baseReward = remember { noteEvent.getReward()?.let { Reward(it) } }
|
||||
if (baseReward != null) {
|
||||
DisplayReward(baseReward, baseNote, accountViewModel, nav)
|
||||
|
||||
@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@@ -85,6 +84,7 @@ import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
@@ -94,7 +94,6 @@ import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
@@ -127,7 +126,9 @@ import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldLeadingIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
@@ -196,7 +197,7 @@ fun ChannelScreen(
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
NostrChannelDataSource.start()
|
||||
feedViewModel.invalidateData()
|
||||
feedViewModel.invalidateData(true)
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
newPostModel.imageUploadingError.collect { error ->
|
||||
@@ -212,7 +213,7 @@ fun ChannelScreen(
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Channel Start")
|
||||
NostrChannelDataSource.start()
|
||||
feedViewModel.invalidateData()
|
||||
feedViewModel.invalidateData(true)
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Channel Stop")
|
||||
@@ -229,14 +230,6 @@ fun ChannelScreen(
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
ChannelHeader(
|
||||
baseChannel = channel,
|
||||
showVideo = true,
|
||||
showBottomDiviser = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
|
||||
val replyTo = remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
Column(
|
||||
@@ -571,6 +564,7 @@ fun ChannelHeader(
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = modifier.clickable {
|
||||
if (sendToChannel) {
|
||||
nav(routeFor(baseChannel))
|
||||
@@ -670,7 +664,7 @@ private fun ShortChannelHeader(
|
||||
channel.creator?.let {
|
||||
UserPicture(
|
||||
user = it,
|
||||
size = Size35dp,
|
||||
size = Size34dp,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
@@ -682,11 +676,7 @@ private fun ShortChannelHeader(
|
||||
model = it,
|
||||
contentDescription = stringResource(R.string.profile_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.width(Size35dp)
|
||||
.height(Size35dp)
|
||||
.clip(shape = CircleShape)
|
||||
modifier = HeaderPictureModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Divider
|
||||
@@ -36,7 +38,8 @@ import com.vitorpamplona.amethyst.ui.note.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -129,10 +132,7 @@ fun ChatroomScreen(
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
ChatroomHeader(baseUser, accountViewModel, nav = nav)
|
||||
|
||||
val replyTo = remember { mutableStateOf<Note?>(null) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
@@ -179,34 +179,36 @@ fun ChatroomScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun ChatroomHeader(
|
||||
baseUser: User,
|
||||
modifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.clickable(
|
||||
modifier = Modifier.fillMaxWidth().clickable(
|
||||
onClick = { nav("User/${baseUser.pubkeyHex}") }
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = modifier
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ClickableUserPicture(
|
||||
baseUser = baseUser,
|
||||
accountViewModel = accountViewModel,
|
||||
size = Size35dp
|
||||
size = Size34dp
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
UsernameDisplay(baseUser)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ObserveDisplayNip05Status(baseUser)
|
||||
}
|
||||
UsernameDisplay(baseUser)
|
||||
ObserveDisplayNip05Status(baseUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
@@ -15,39 +13,24 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.NostrCommunityDataSource
|
||||
import com.vitorpamplona.amethyst.ui.note.CommunityHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrCommunityFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun CommunityScreen(aTagHex: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
if (aTagHex == null) return
|
||||
|
||||
var noteDefBase by remember { mutableStateOf<AddressableNote?>(LocalCache.getAddressableNoteIfExists(aTagHex)) }
|
||||
|
||||
if (noteDefBase == null) {
|
||||
LaunchedEffect(aTagHex) {
|
||||
// waits to resolve.
|
||||
launch(Dispatchers.IO) {
|
||||
val newNote = LocalCache.checkGetOrCreateAddressableNote(aTagHex)
|
||||
if (newNote != noteDefBase) {
|
||||
noteDefBase = newNote
|
||||
}
|
||||
}
|
||||
LoadAddressableNote(aTagHex = aTagHex) {
|
||||
it?.let {
|
||||
PrepareViewModelsCommunityScreen(
|
||||
note = it,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
noteDefBase?.let {
|
||||
PrepareViewModelsCommunityScreen(
|
||||
note = it,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -94,13 +77,6 @@ fun CommunityScreen(note: AddressableNote, feedViewModel: NostrCommunityFeedView
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
CommunityHeader(
|
||||
baseNote = note,
|
||||
showBottomDiviser = true,
|
||||
sendToCommunity = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
RefresheableFeedView(
|
||||
feedViewModel,
|
||||
null,
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
@@ -34,7 +35,7 @@ import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
|
||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrGeoHashFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -96,7 +97,6 @@ fun GeoHashScreen(tag: String, feedViewModel: NostrGeoHashFeedViewModel, account
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
GeoHashHeader(tag, accountViewModel)
|
||||
RefresheableFeedView(
|
||||
feedViewModel,
|
||||
null,
|
||||
@@ -108,46 +108,51 @@ fun GeoHashScreen(tag: String, feedViewModel: NostrGeoHashFeedViewModel, account
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GeoHashHeader(tag: String, account: AccountViewModel, onClick: () -> Unit = { }) {
|
||||
fun GeoHashHeader(tag: String, modifier: Modifier = StdPadding, account: AccountViewModel, onClick: () -> Unit = { }) {
|
||||
Column(
|
||||
Modifier.clickable { onClick() }
|
||||
Modifier.fillMaxWidth().clickable { onClick() }
|
||||
) {
|
||||
Column(modifier = HalfPadding) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val cityName = remember(tag) {
|
||||
ReverseGeoLocationUtil().execute(tag.toGeoHash().toLocation(), context)
|
||||
}
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
DislayGeoTagHeader(tag, remember { Modifier.weight(1f) })
|
||||
|
||||
Text(
|
||||
"$cityName ($tag)",
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
HashtagActionOptions(tag, account)
|
||||
}
|
||||
}
|
||||
HashtagActionOptions(tag, account)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DislayGeoTagHeader(geohash: String, modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var cityName by remember(geohash) {
|
||||
mutableStateOf<String>(geohash)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = geohash) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newCityName = ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)?.ifBlank { null }
|
||||
if (newCityName != null && newCityName != cityName) {
|
||||
cityName = "$newCityName ($geohash)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
cityName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HashtagActionOptions(
|
||||
tag: String,
|
||||
|
||||
@@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -93,7 +93,6 @@ fun HashtagScreen(tag: String, feedViewModel: NostrHashtagFeedViewModel, account
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
) {
|
||||
HashtagHeader(tag, accountViewModel)
|
||||
RefresheableFeedView(
|
||||
feedViewModel,
|
||||
null,
|
||||
@@ -105,36 +104,26 @@ fun HashtagScreen(tag: String, feedViewModel: NostrHashtagFeedViewModel, account
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HashtagHeader(tag: String, account: AccountViewModel, onClick: () -> Unit = { }) {
|
||||
fun HashtagHeader(tag: String, modifier: Modifier = StdPadding, account: AccountViewModel, onClick: () -> Unit = { }) {
|
||||
Column(
|
||||
Modifier.clickable { onClick() }
|
||||
Modifier.fillMaxWidth().clickable { onClick() }
|
||||
) {
|
||||
Column(modifier = HalfPadding) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"#$tag",
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
"#$tag",
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
HashtagActionOptions(tag, account)
|
||||
}
|
||||
}
|
||||
HashtagActionOptions(tag, account)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
}
|
||||
|
||||
+10
-4
@@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.RoyalBlue
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
@@ -243,14 +244,19 @@ class AmountAxisValueFormatter() : AxisValueFormatter<AxisPosition.Vertical.End>
|
||||
}
|
||||
}
|
||||
|
||||
var dfG: DecimalFormat = DecimalFormat("#G")
|
||||
var dfM: DecimalFormat = DecimalFormat("#M")
|
||||
var dfK: DecimalFormat = DecimalFormat("#k")
|
||||
var dfN: DecimalFormat = DecimalFormat("#")
|
||||
|
||||
fun showAmountAxis(amount: BigDecimal?): String {
|
||||
if (amount == null) return ""
|
||||
if (amount.abs() < BigDecimal(0.01)) return ""
|
||||
|
||||
return when {
|
||||
amount >= OneGiga -> "%.0fG".format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP))
|
||||
amount >= OneMega -> "%.0fM".format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP))
|
||||
amount >= OneKilo -> "%.0fk".format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP))
|
||||
else -> "%.0f".format(amount)
|
||||
amount >= OneGiga -> dfG.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP))
|
||||
amount >= OneMega -> dfM.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP))
|
||||
amount >= OneKilo -> dfK.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP))
|
||||
else -> dfN.format(amount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ val HalfDoubleVertSpacer = Modifier.height(7.dp)
|
||||
val Size0dp = 0.dp
|
||||
val Size5dp = 5.dp
|
||||
val Size10dp = 10.dp
|
||||
val Size12dp = 12.dp
|
||||
val Size13dp = 13.dp
|
||||
val Size15dp = 15.dp
|
||||
val Size16dp = 16.dp
|
||||
@@ -59,6 +60,7 @@ val Size22dp = 22.dp
|
||||
val Size24dp = 24.dp
|
||||
val Size25dp = 25.dp
|
||||
val Size30dp = 30.dp
|
||||
val Size34dp = 34.dp
|
||||
val Size35dp = 35.dp
|
||||
val Size55dp = 55.dp
|
||||
val Size75dp = 75.dp
|
||||
@@ -104,7 +106,8 @@ val UserNameMaxRowHeight = Modifier.fillMaxWidth()
|
||||
|
||||
val Height4dpModifier = Modifier.height(4.dp)
|
||||
|
||||
val AccountPictureModifier = Modifier.width(55.dp).height(55.dp).clip(shape = CircleShape)
|
||||
val AccountPictureModifier = Modifier.size(55.dp).clip(shape = CircleShape)
|
||||
val HeaderPictureModifier = Modifier.size(34.dp).clip(shape = CircleShape)
|
||||
|
||||
val ShowMoreRelaysButtonIconButtonModifier = Modifier.size(24.dp)
|
||||
val ShowMoreRelaysButtonIconModifier = Modifier.size(15.dp)
|
||||
|
||||
@@ -30,6 +30,7 @@ val Typography = Typography(
|
||||
*/
|
||||
)
|
||||
|
||||
val Font12SP = 12.sp
|
||||
val Font14SP = 14.sp
|
||||
val Font17SP = 17.sp
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="point_to_the_qr_code">Pointer vers le QR code</string>
|
||||
<string name="show_qr">Montrer le QR code</string>
|
||||
<string name="profile_image">Image de profil</string>
|
||||
@@ -92,7 +92,7 @@
|
||||
<string name="image_saved_to_the_gallery">Image enregistrée dans la galerie</string>
|
||||
<string name="failed_to_save_the_image">Échec de l\'enregistrement de l\'image</string>
|
||||
<string name="upload_image">Charger une image</string>
|
||||
<string name="uploading">Chargement...</string>
|
||||
<string name="uploading">Chargement…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">L\'utilisateur n\'a pas configuré d\'adresse Lightning pour recevoir des sats</string>
|
||||
<string name="reply_here">"Répondre ici.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copie l\'ID de note dans le presse-papiers pour le partage</string>
|
||||
@@ -169,7 +169,11 @@
|
||||
<string name="report_hateful_speech">Signaler Discours haineux</string>
|
||||
<string name="report_nudity_porn">Signaler Nudité / Porno</string>
|
||||
<string name="others">autres</string>
|
||||
<string name="account_backup_tips_md">
|
||||
<string name="mark_all_known_as_read">Marquer tous les Connus comme lu</string>
|
||||
<string name="mark_all_new_as_read">Marquer tous les Nouveaux comme lu</string>
|
||||
<string name="mark_all_as_read">Tout marquer comme lu</string>
|
||||
<string name="backup_keys">Clés de sauvegarde</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos">
|
||||
## Sauvegarde des clés et conseils de sécurité
|
||||
\n\nVotre compte est sécurisé par une clé secrète. La clé est une longue chaîne aléatoire commençant par **nsec1**. Toute personne ayant accès à votre clé secrète peut publier du contenu en utilisant votre identité.
|
||||
\n\n- Ne communiquez **jamais** votre clé secrète à un site Web ou un logiciel auquel vous ne faites pas confiance.
|
||||
@@ -177,5 +181,322 @@
|
||||
\n- **Conservez** une sauvegarde sécurisée de votre clé secrète pour la récupération de compte. Nous vous recommandons d\'utiliser un gestionnaire de mots de passe.
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">Clé secrète (nsec) copiée dans le presse-papiers</string>
|
||||
<string name="backup_keys">Clés de sauvegarde</string>
|
||||
<string name="copy_my_secret_key">Copier ma clé secrète</string>
|
||||
<string name="biometric_authentication_failed">Échec de l\'authentification</string>
|
||||
<string name="biometric_error">Erreur</string>
|
||||
<string name="badge_created_by">"Créé par %1$s"</string>
|
||||
<string name="badge_award_image_for">"Image de badge pour %1$s"</string>
|
||||
<string name="new_badge_award_notif">Vous avez reçu un nouveau Badge</string>
|
||||
<string name="award_granted_to">Le Badge a été décerné à</string>
|
||||
<string name="copied_note_text_to_clipboard">Copie du contenu de la note dans le presse-papiers</string>
|
||||
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">Copie du @npub utilisateur dans le presse-papiers</string>
|
||||
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">Copie de l\'ID de la note (@note1) dans le presse-papiers</string>
|
||||
<string name="select_text_dialog_top">Sélectionner le Texte</string>
|
||||
|
||||
<string name="private_conversation_notification">"<Impossible de déchiffrer le message privé>\n\nVous avez été cité dans une conversation privée/cryptée entre %1$s et %2$s."</string>
|
||||
<string name="account_switch_add_account_dialog_title">Ajouter un Nouveau Compte</string>
|
||||
<string name="drawer_accounts">Comptes</string>
|
||||
<string name="account_switch_select_account">Sélectionner un Compte</string>
|
||||
<string name="account_switch_add_account_btn">Ajouter un Nouveau Compte</string>
|
||||
<string name="account_switch_active_account">Compte actif</string>
|
||||
<string name="account_switch_has_private_key">Avec une clé privée</string>
|
||||
<string name="account_switch_pubkey_only">Lecture seule, pas de clé privée</string>
|
||||
<string name="back">Retour</string>
|
||||
<string name="quick_action_select">Sélectionner</string>
|
||||
<string name="quick_action_share_browser_link">Partager le Lien Navigateur</string>
|
||||
<string name="quick_action_share">Partager</string>
|
||||
<string name="quick_action_copy_user_id">ID Author</string>
|
||||
<string name="quick_action_copy_note_id">ID Note</string>
|
||||
<string name="quick_action_copy_text">Copier le Texte</string>
|
||||
<string name="quick_action_delete">Supprimer</string>
|
||||
<string name="quick_action_unfollow">Ne plus suivre</string>
|
||||
<string name="quick_action_follow">Suivre</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Demande de Suppression</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst demandera que votre note soit supprimée des relais auxquels vous êtes actuellement connecté. Il n\'y a aucune garantie que votre note sera définitivement supprimée de ces relais, ou d\'autres relais où elle peut être stockée.</string>
|
||||
<string name="quick_action_block_dialog_btn">Bloquer</string>
|
||||
<string name="quick_action_delete_dialog_btn">Supprimer</string>
|
||||
<string name="quick_action_block">Bloquer</string>
|
||||
<string name="quick_action_report">Signaler</string>
|
||||
<string name="quick_action_delete_button">Supprimer</string>
|
||||
<string name="quick_action_dont_show_again_button">Ne plus montrer</string>
|
||||
<string name="report_dialog_spam">Spam ou arnaque</string>
|
||||
<string name="report_dialog_profanity">Grossièreté ou contenu haineux</string>
|
||||
<string name="report_dialog_impersonation">Usurpation d\'identité</string>
|
||||
<string name="report_dialog_nudity">Nudité ou contenu graphique</string>
|
||||
<string name="report_dialog_illegal">Comportement illégal</string>
|
||||
<string name="report_dialog_blocking_a_user">Bloquer un utilisateur cachera son contenu dans votre application. Vos notes sont toujours visibles publiquement, y compris pour les personnes que vous bloquez. Les utilisateurs bloqués sont listés sur l\'écran Filtres de Sécurité.</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Bloquer et Masquer l\'Utilisateur]]></string>
|
||||
<string name="report_dialog_report_btn">Signaler un Abus</string>
|
||||
<string name="report_dialog_reminder_public">Tous les rapports publiés seront visibles publiquement.</string>
|
||||
<string name="report_dialog_additional_reason_placeholder">Fournir un contexte optionnel supplémentaire sur votre signalement…</string>
|
||||
<string name="report_dialog_additional_reason_label">Contexte additionnel</string>
|
||||
<string name="report_dialog_select_reason_label">Raison</string>
|
||||
<string name="report_dialog_select_reason_placeholder">Sélectionner une raison…</string>
|
||||
<string name="report_dialog_post_report_btn">Publier le signalement</string>
|
||||
<string name="report_dialog_title">Bloquer et Signaler</string>
|
||||
<string name="block_only">Bloquer</string>
|
||||
|
||||
|
||||
<string name="bookmarks">Favoris</string>
|
||||
<string name="private_bookmarks">Favoris Privés</string>
|
||||
<string name="public_bookmarks">Favoris Publics</string>
|
||||
<string name="add_to_private_bookmarks">Ajouter aux Favoris Privés</string>
|
||||
<string name="add_to_public_bookmarks">Ajouter aux Favoris Publics</string>
|
||||
<string name="remove_from_private_bookmarks">Supprimer des Favoris Privés</string>
|
||||
<string name="remove_from_public_bookmarks">Supprimer des Favoris Publics</string>
|
||||
|
||||
<string name="wallet_connect_service">Service Wallet Connect</string>
|
||||
<string name="wallet_connect_service_explainer">Autorise un Nostr Secret à payer des zaps sans quitter l\'application. Gardez le secret en toute sécurité et utilisez un relais privé si possible</string>
|
||||
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
|
||||
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
|
||||
<string name="wallet_connect_service_secret">Wallet Connect Secret</string>
|
||||
<string name="wallet_connect_service_show_secret">Montrer la clé secrète</string>
|
||||
<string name="wallet_connect_service_secret_placeholder">Clé privée nsec / hex</string>
|
||||
|
||||
<string name="pledge_amount_in_sats">Montant engagé en Sats</string>
|
||||
<string name="post_poll">Message Sondage</string>
|
||||
<string name="poll_heading_required">Champs requis:</string>
|
||||
<string name="poll_zap_recipients">Bénéficiaires du Zap</string>
|
||||
<string name="poll_primary_description">Description du sondage primaire…</string>
|
||||
<string name="poll_option_index">Option %s</string>
|
||||
<string name="poll_option_description">Description de l\'option du Sondage</string>
|
||||
<string name="poll_heading_optional">Champs optionels:</string>
|
||||
<string name="poll_zap_value_min">Zap minimum</string>
|
||||
<string name="poll_zap_value_max">Zap maximum</string>
|
||||
<string name="poll_consensus_threshold">Consensus</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Clôturer après</string>
|
||||
<string name="poll_closing_time_days">jours</string>
|
||||
<string name="poll_is_closed">Le Sondage est fermé aux nouveaux votes</string>
|
||||
<string name="poll_zap_amount">Montant des Zap</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Un seul vote par utilisateur est autorisé sur ce type de sondage</string>
|
||||
|
||||
<string name="looking_for_event">"Recherche de l'Evénement %1$s"</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message">Ajouter un message public</string>
|
||||
<string name="custom_zaps_add_a_message_private">Ajouter un message privé</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">Ajouter un message facture</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message_example">Merci pour tout votre travail !</string>
|
||||
|
||||
<string name="lightning_create_and_add_invoice">Créer et Ajouter</string>
|
||||
<string name="poll_author_no_vote">Les auteurs du sondage ne peuvent pas voter dans leurs propres sondages.</string>
|
||||
|
||||
<string name="hash_verification_passed">Ce contenu est le même depuis le message</string>
|
||||
<string name="hash_verification_failed">Ce contenu a changé. L\'auteur n\'a peut-être pas vu ou approuvé le changement</string>
|
||||
|
||||
<string name="content_description_add_image">Ajouter une Image</string>
|
||||
<string name="content_description_add_video">Ajouter une Vidéo</string>
|
||||
<string name="content_description_add_document">Ajouter un Document</string>
|
||||
|
||||
<string name="add_content">Ajouter au Message</string>
|
||||
<string name="content_description">Description des contenus</string>
|
||||
<string name="content_description_example">Un bateau bleu sur une plage de sable blanc au coucher du soleil</string>
|
||||
|
||||
|
||||
<string name="zap_type">Type de Zap</string>
|
||||
<string name="zap_type_explainer">Zap Type pour toutes les options</string>
|
||||
|
||||
<string name="zap_type_public">Public</string>
|
||||
<string name="zap_type_public_explainer">Tout le monde peut voir la transaction et le message</string>
|
||||
|
||||
<string name="zap_type_private">Privé</string>
|
||||
<string name="zap_type_private_explainer">L\'expéditeur et le receveur peuvent se voir et lire le message</string>
|
||||
|
||||
<string name="zap_type_anonymous">Anonyme</string>
|
||||
<string name="zap_type_anonymous_explainer">Le receveur et le public ne savent pas qui a envoyé le paiement</string>
|
||||
|
||||
<string name="zap_type_nonzap">Non-Zap</string>
|
||||
<string name="zap_type_nonzap_explainer">Aucune trace sur Nostr, seulement sur le Lightning</string>
|
||||
|
||||
|
||||
<string name="file_server">Serveur fichier</string>
|
||||
<string name="zap_forward_lnAddress">Adresse LN ou @Utilisateur</string>
|
||||
|
||||
<string name="upload_server_imgur">imgur.com - de confiance</string>
|
||||
<string name="upload_server_imgur_explainer">Imgur peut modifier le fichier</string>
|
||||
|
||||
<string name="upload_server_nostrimg">nostrimg.com - de confiance</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImg peut modifier le fichier</string>
|
||||
|
||||
<string name="upload_server_nostrbuild">nostr.build - de confiance</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.build peut modifier le fichier</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - de confiance</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.dev peut modifier le fichier</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - de confiance</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.me peut modifier le fichier</string>
|
||||
|
||||
|
||||
<string name="upload_server_imgur_nip94">Verifiable Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Vérifie si Imgur a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
|
||||
<string name="upload_server_nostrimg_nip94">Verifiable NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">Vérifie si NostrImg a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
|
||||
<string name="upload_server_nostrbuild_nip94">Verifiable Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Vérifie si Nostr.build a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev_nip94">Verifiable Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Vérifie si Nostrfiles.dev a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme_nip94">Verifiable Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Vérifie si Nostrcheck.me a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir</string>
|
||||
|
||||
<string name="upload_server_relays_nip95">Vos relais (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Les fichiers sont hébergés par vos relais. Nouveau NIP: vérifiez s\'ils sont supportés</string>
|
||||
|
||||
<string name="connect_via_tor_short">Configuration Tor/Orbot</string>
|
||||
<string name="connect_via_tor">Se connecter à travers la configuration Orbot</string>
|
||||
|
||||
<string name="do_you_really_want_to_disable_tor_title">Se déconnecter de Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Vos données seront immédiatement transférées sur le réseau classique</string>
|
||||
<string name="yes">Oui</string>
|
||||
<string name="no">Non</string>
|
||||
|
||||
|
||||
<string name="follow_list_selection">Liste des Abonnés</string>
|
||||
<string name="follow_list_kind3follows">Tous les abonnements</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## Se connecter à travers Tor avec Orbot
|
||||
\n\n1. Installe [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Démarre Orbot
|
||||
\n3. Dans Orbot, vérifie le port Socks. Par défaut 9050
|
||||
\n4. Si nécessaire change le port dans Orbot
|
||||
\n5. Configure le port Socks sur cet écran
|
||||
\n6. Appuyer sur le bouton Activer pour utiliser Orbot comme proxy
|
||||
</string>
|
||||
<string name="orbot_socks_port">Port Socks Orbot</string>
|
||||
<string name="invalid_port_number">Numéro de port invalide</string>
|
||||
<string name="use_orbot">Utiliser Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Déconnecter Tor/Orbot</string>
|
||||
|
||||
<string name="app_notification_dms_channel_name">Messages privés</string>
|
||||
<string name="app_notification_dms_channel_description">Vous notifie lorsqu\'un message privé est reçu</string>
|
||||
|
||||
<string name="app_notification_zaps_channel_name">Zaps reçus</string>
|
||||
<string name="app_notification_zaps_channel_description">Vous notifie lorsque quelqu\'un vous zap</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">De %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">pour %1$s</string>
|
||||
|
||||
|
||||
<string name="reply_notify">Notifier: </string>
|
||||
|
||||
<string name="channel_list_join_conversation">Rejoindre Conversation</string>
|
||||
<string name="channel_list_user_or_group_id">Utilisateur ou Groupe ID</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent ou hex</string>
|
||||
<string name="channel_list_create_channel">Créer</string>
|
||||
<string name="channel_list_join_channel">Rejoindre</string>
|
||||
|
||||
<string name="today">Aujourd\'hui</string>
|
||||
|
||||
<string name="content_warning">Avertissement de contenu</string>
|
||||
<string name="content_warning_explanation">Ce message contient du contenu sensible que certaines personnes peuvent trouver offensant ou dérangeant</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">Toujours cacher les contenus sensibles</string>
|
||||
<string name="content_warning_show_all_sensitive_content">Toujours afficher les contenus sensibles</string>
|
||||
<string name="content_warning_see_warnings">Toujours afficher les avertissements de contenu</string>
|
||||
|
||||
<string name="recommended_apps">Recommendations: </string>
|
||||
<string name="filter_spam_from_strangers">Filtrer le spam des étrangers</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Avertir lorsque les messages ont été rapportés par vos abonnés</string>
|
||||
|
||||
<string name="new_reaction_symbol">Nouveau symbole de réaction</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">Aucun type de réaction sélectionné. Appuyer longuement pour changer</string>
|
||||
|
||||
<string name="zapraiser">Zapraiser</string>
|
||||
<string name="zapraiser_explainer">Ajoute une quantité cible de sats pour ce message. Les clients compatible peuvent afficher une barre de progrès pour encourager les dons</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Quantité ciblée en Sats</string>
|
||||
|
||||
<string name="sats_to_complete">Zapraiser à %1$s. %2$s sats pour objectif</string>
|
||||
<string name="read_from_relay">Lire depuis le Relay</string>
|
||||
<string name="write_to_relay">Écrire sur le Relay</string>
|
||||
<string name="an_error_occurred_trying_to_get_relay_information">Une erreur s\'est produite en récupérant les informations du relay depuis %1$s</string>
|
||||
<string name="owner">Propriétaire</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="software">Logiciel</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="supports">NIPs supportés</string>
|
||||
<string name="admission_fees">Frais d\'admission</string>
|
||||
<string name="payments_url">URL de payments</string>
|
||||
<string name="limitations">Limitations</string>
|
||||
<string name="countries">Pays</string>
|
||||
<string name="languages">Langages</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="posting_policy">Politique de publication</string>
|
||||
<string name="message_length">Longueur du message</string>
|
||||
<string name="subscriptions">Abonnements</string>
|
||||
<string name="filters">Filtres</string>
|
||||
<string name="subscription_id_length">Longueur de l\'id de souscription</string>
|
||||
<string name="minimum_prefix">Préfixe minimum</string>
|
||||
<string name="maximum_event_tags">Tags d\'événement maximum</string>
|
||||
<string name="content_length">Longueur du contenu</string>
|
||||
<string name="minimum_pow">PoW minimum</string>
|
||||
<string name="auth">Authentification</string>
|
||||
<string name="payment">Paiement</string>
|
||||
<string name="cashu">Jeton Cashu</string>
|
||||
<string name="cashu_redeem">Échanger</string>
|
||||
<string name="no_lightning_address_set">Adresse Lightning non définie</string>
|
||||
<string name="copied_token_to_clipboard">Jeton copié dans le presse-papiers</string>
|
||||
|
||||
<string name="live_stream_live_tag">EN DIRECT</string>
|
||||
<string name="live_stream_offline_tag">DÉCONNECTÉ</string>
|
||||
<string name="live_stream_ended_tag">TERMINÉ</string>
|
||||
<string name="live_stream_planned_tag">PLANIFIÉ</string>
|
||||
|
||||
<string name="live_stream_is_offline">Livestream est déconnecté</string>
|
||||
<string name="live_stream_has_ended">Livestream terminé</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">Se déconnecter supprime toutes vos informations locales. Assurez-vous d\'avoir vos clés privées sauvegardées pour éviter de perdre votre compte. Voulez-vous continuer ?</string>
|
||||
<string name="followed_tags">Tags suivis</string>
|
||||
|
||||
<string name="relay_setup">Relays</string>
|
||||
|
||||
<string name="discover_live">Direct</string>
|
||||
<string name="discover_community">Communauté</string>
|
||||
<string name="discover_chat">Chats</string>
|
||||
<string name="community_approved_posts">Messages approuvés</string>
|
||||
|
||||
<string name="groups_no_descriptor">Ce groupe n\'a pas de description ou de règles. Parlez au propriétaire pour en ajouter une</string>
|
||||
<string name="community_no_descriptor">Cette communauté n\'a pas de description. Parlez au propriétaire pour en ajouter une</string>
|
||||
|
||||
<string name="add_sensitive_content_label">Contenu sensible</string>
|
||||
<string name="add_sensitive_content_description">Ajouter un avertissement de contenu sensible avant de montrer ce contenu</string>
|
||||
<string name="settings">Paramètres</string>
|
||||
<string name="connectivity_type_always">Toujours</string>
|
||||
<string name="connectivity_type_wifi_only">Wifi uniquement</string>
|
||||
<string name="connectivity_type_never">Jamais</string>
|
||||
|
||||
<string name="system">Système</string>
|
||||
<string name="light">Clair</string>
|
||||
<string name="dark">Sombre</string>
|
||||
<string name="application_preferences">Préférences de l\'application</string>
|
||||
<string name="language">Langage</string>
|
||||
<string name="theme">Thème</string>
|
||||
<string name="automatically_load_images_gifs">Chargement automatique des images/gifs</string>
|
||||
<string name="automatically_play_videos">Démarrage automatique des vidéos</string>
|
||||
<string name="automatically_show_url_preview">Prévisualisation automatique des liens</string>
|
||||
<string name="load_image">Charger l\'image</string>
|
||||
|
||||
<string name="spamming_users">Spammeurs</string>
|
||||
|
||||
<string name="muted_button">Silencieux. Cliquer pour réactiver le son</string>
|
||||
<string name="mute_button">Son activé. Cliquer pour mettre en silencieux</string>
|
||||
<string name="search_button">Recherche d\'enregistrements locaux et distants</string>
|
||||
|
||||
<string name="nip05_verified">L\'adresse Nostr a été vérfiée</string>
|
||||
<string name="nip05_failed">La vérification de l\'adresse Nostr a échouée</string>
|
||||
<string name="nip05_checking">Vérification de l\'adresse Nostr</string>
|
||||
<string name="select_deselect_all">Tout sélectionner/désélectionner</string>
|
||||
<string name="default_relays">Défaut</string>
|
||||
<string name="select_a_relay_to_continue">Sélectionner un relay pour continuer</string>
|
||||
|
||||
<string name="zap_forward_title">Transférer les Zaps à:</string>
|
||||
<string name="zap_forward_explainer">Les clients compatibles transmettront des zaps sur l\'adresse LN ou le profil Utilisateur ci-dessous au lieu du vôtre.</string>
|
||||
|
||||
<string name="geohash_title">Révéler la localisation comme </string>
|
||||
<string name="geohash_explainer">Ajoute un Geohash de votre emplacement au message. Le public saura que vous êtes à moins de 5km de l\'emplacement actuel</string>
|
||||
|
||||
<string name="add_sensitive_content_explainer">Ajoute un avertissement de contenu sensible avant de montrer votre contenu. C\'est idéal pour tout contenu NSFW ou contenu que certaines personnes peuvent trouver offensant ou dérangeant</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user