diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d33521a..000000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index d39e145c1..000000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-Amethyst
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index d7cb58b81..000000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index b1a56d722..ba010569c 100644
--- a/README.md
+++ b/README.md
@@ -75,10 +75,13 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
- [x] Relay Pages (NIP-11)
- [x] HTTP Auth (NIP-98)
- [x] Zapraiser (NIP-TBD)
-- [x] Moderated Communities (NIP-172)
+- [x] Moderated Communities (NIP-72)
- [x] Emoji Packs (Kind:30030)
- [x] Personal Emoji Lists (Kind:10030)
- [x] Classifieds (Kind:30403)
+- [x] Private Messages and Small Groups (NIP-24)
+- [x] Gift Wraps & Seals (NIP-59)
+- [x] Versioned Encrypted Payloads (NIP-44)
- [ ] Marketplace (NIP-15)
- [ ] Image/Video Capture in the app
- [ ] Local Database
diff --git a/app/build.gradle b/app/build.gradle
index 954385bb0..e711d3577 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
- id 'org.jlleitschuh.gradle.ktlint' version "11.5.0"
+ id 'org.jlleitschuh.gradle.ktlint' version "11.5.1"
id 'com.google.gms.google-services'
}
@@ -179,7 +179,7 @@ dependencies {
playImplementation 'com.google.mlkit:translate:17.0.1'
// PushNotifications
- playImplementation platform('com.google.firebase:firebase-bom:32.2.0')
+ playImplementation platform('com.google.firebase:firebase-bom:32.2.2')
playImplementation 'com.google.firebase:firebase-messaging-ktx'
// Charts
@@ -204,7 +204,7 @@ dependencies {
implementation 'id.zelory:compressor:3.0.1'
testImplementation 'junit:junit:4.13.2'
- testImplementation 'io.mockk:mockk:1.13.5'
+ testImplementation 'io.mockk:mockk:1.13.7'
androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha01'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt
index bb25fe513..48ea82a2b 100644
--- a/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt
+++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt
@@ -1,113 +1,78 @@
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.vitorpamplona.amethyst.model.Account
+import com.vitorpamplona.amethyst.service.KeyPair
+import com.vitorpamplona.amethyst.ui.actions.FileServer
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
import com.vitorpamplona.amethyst.ui.actions.ImgurServer
import com.vitorpamplona.amethyst.ui.actions.NostrBuildServer
import com.vitorpamplona.amethyst.ui.actions.NostrFilesDevServer
import com.vitorpamplona.amethyst.ui.actions.NostrImgServer
-import junit.framework.TestCase.assertNotNull
-import junit.framework.TestCase.fail
-import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
+import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import java.util.Base64
+import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class)
class ImageUploadTesting {
val image = "R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzWlwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2ccMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjAJ8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8AAF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMuQeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSHpzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGRs/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78AAAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMiwocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7GnwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euTeJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dtGCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWlMc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPeiUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYIm4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZcNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3ADTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kVMyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDGqCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMWZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bDGdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB776aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJHgxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiAFB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPAgCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHgrhGSQJxCS+0pCZbEhAAOw=="
- @Test()
- fun testImgurUpload() = runBlocking {
+ fun testBase(server: FileServer) {
val bytes = Base64.getDecoder().decode(image)
val inputStream = bytes.inputStream()
+ val countDownLatch = CountDownLatch(1)
+ var url: String? = null
+ var error: String? = null
+
+ ImageUploader.account = Account(
+ KeyPair()
+ )
+
ImageUploader.uploadImage(
inputStream,
bytes.size.toLong(),
"image/gif",
- ImgurServer(),
- onSuccess = { url, contentType ->
+ server,
+ onSuccess = { newUrl, contentType ->
println("Uploaded to $url")
- assertNotNull(url)
+ url = newUrl
+ countDownLatch.countDown()
},
onError = {
println("Failed to Upload")
- fail("${it.message}")
+ error = it.message
+ countDownLatch.countDown()
}
)
- delay(5000)
+ countDownLatch.await()
+
+ Assert.assertNull(error)
+ Assert.assertTrue(url?.startsWith("http") == true)
+ }
+
+ @Test()
+ fun testImgurUpload() = runBlocking {
+ testBase(ImgurServer())
}
@Test()
fun testNostrBuildUpload() = runBlocking {
- val bytes = Base64.getDecoder().decode(image)
- val inputStream = bytes.inputStream()
-
- ImageUploader.uploadImage(
- inputStream,
- bytes.size.toLong(),
- "image/gif",
- NostrBuildServer(),
- onSuccess = { url, contentType ->
- println("Uploaded to $url")
- assertNotNull(url)
- },
- onError = {
- println("Failed to Upload")
- fail("${it.message}")
- }
- )
-
- delay(1000)
+ testBase(NostrBuildServer())
}
@Test()
fun testNostrImgUpload() = runBlocking {
- val bytes = Base64.getDecoder().decode(image)
- val inputStream = bytes.inputStream()
-
- ImageUploader.uploadImage(
- inputStream,
- bytes.size.toLong(),
- "image/gif",
- NostrImgServer(),
- onSuccess = { url, contentType ->
- println("Uploaded to $url")
- assertNotNull(url)
- },
- onError = {
- println("Failed to Upload")
- fail("${it.message}")
- }
- )
-
- delay(1000)
+ testBase(NostrImgServer())
}
@Test()
fun testNostrFilesDevUpload() = runBlocking {
- val bytes = Base64.getDecoder().decode(image)
- val inputStream = bytes.inputStream()
-
- ImageUploader.uploadImage(
- inputStream,
- bytes.size.toLong(),
- "image/gif",
- NostrFilesDevServer(),
- onSuccess = { url, contentType ->
- println("Uploaded to $url")
- assertNotNull(url)
- },
- onError = {
- println("Failed to Upload")
- fail("${it.message}")
- }
- )
-
- delay(5000)
+ testBase(NostrFilesDevServer())
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
index 00d9e598e..67c2d0226 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
@@ -74,8 +74,8 @@ class Account(
var hideBlockAlertDialog: Boolean = false,
var hideNIP24WarningDialog: Boolean = false,
var backupContactList: ContactListEvent? = null,
- var proxy: Proxy?,
- var proxyPort: Int,
+ var proxy: Proxy? = null,
+ var proxyPort: Int = 9050,
var showSensitiveContent: Boolean? = null,
var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true,
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
index 59e413f8d..0664e2bb5 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
@@ -220,6 +220,26 @@ object LocalCache {
}
}
+ private fun consume(event: AdvertisedRelayListEvent) {
+ val version = getOrCreateNote(event.id)
+ val note = getOrCreateAddressableNote(event.address())
+ val author = getOrCreateUser(event.pubKey)
+
+ if (version.event == null) {
+ version.loadEvent(event, author, emptyList())
+ version.moveAllReferencesTo(note)
+ }
+
+ // Already processed this event.
+ if (note.event?.id() == event.id()) return
+
+ if (event.createdAt > (note.createdAt() ?: 0)) {
+ note.loadEvent(event, author, emptyList())
+
+ refreshObservers(note)
+ }
+ }
+
fun formattedDateTime(timestamp: Long): String {
return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
@@ -1497,6 +1517,7 @@ object LocalCache {
try {
when (event) {
+ is AdvertisedRelayListEvent -> consume(event)
is AppDefinitionEvent -> consume(event)
is AppRecommendationEvent -> consume(event)
is AudioTrackEvent -> consume(event)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt
index 3683221a1..7c3ec2c59 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt
@@ -59,7 +59,7 @@ val noProtocolUrlValidator = try {
Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)")
}
-val HTTPRegex = "^((http|https)://)?([A-Za-z0-9-]+(\\.[A-Za-z0-9]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?".toRegex(RegexOption.IGNORE_CASE)
+val HTTPRegex = "^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?".toRegex(RegexOption.IGNORE_CASE)
class RichTextParser() {
fun parseText(
@@ -183,7 +183,7 @@ class RichTextParser() {
} else if (word.contains(".") && 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)
+ 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 {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt
index 9ce1400ac..4ed3941bc 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt
@@ -48,6 +48,17 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
)
}
+ fun createAccountRelayListFilter(): TypedFilter {
+ return TypedFilter(
+ types = COMMON_FEED_TYPES,
+ filter = JsonFilter(
+ kinds = listOf(AdvertisedRelayListEvent.kind),
+ authors = listOf(account.userProfile().pubkeyHex),
+ limit = 1
+ )
+ )
+ }
+
fun createAccountAcceptedAwardsFilter(): TypedFilter {
return TypedFilter(
types = COMMON_FEED_TYPES,
@@ -155,6 +166,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
accountChannel.typedFilters = listOf(
createAccountMetadataFilter(),
createAccountContactListFilter(),
+ createAccountRelayListFilter(),
createNotificationFilter(),
createGiftWrapsToMeFilter(),
createAccountReportsFilter(),
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt
index 970faa9b9..9c66b8729 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt
@@ -2,11 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
-import com.vitorpamplona.amethyst.model.hexToByteArray
-import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.bechToBytes
import com.vitorpamplona.amethyst.service.nip19.Tlv
-import com.vitorpamplona.amethyst.service.nip19.toByteArray
+import com.vitorpamplona.amethyst.service.nip19.TlvBuilder
import com.vitorpamplona.amethyst.service.toNAddress
import fr.acinq.secp256k1.Hex
@@ -15,22 +13,12 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela
fun toTag() = "$kind:$pubKeyHex:$dTag"
fun toNAddr(): String {
- val kind = kind.toByteArray()
- val author = pubKeyHex.hexToByteArray()
- val dTag = dTag.toByteArray(Charsets.UTF_8)
- val relay = relay?.toByteArray(Charsets.UTF_8)
-
- var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, dTag.size.toByte()) + dTag
-
- if (relay != null) {
- fullArray = fullArray + byteArrayOf(Tlv.Type.RELAY.id, relay.size.toByte()) + relay
- }
-
- fullArray = fullArray +
- byteArrayOf(Tlv.Type.AUTHOR.id, author.size.toByte()) + author +
- byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind
-
- return fullArray.toNAddress()
+ return TlvBuilder().apply {
+ addString(Tlv.Type.SPECIAL, dTag)
+ addStringIfNotNull(Tlv.Type.RELAY, relay)
+ addHex(Tlv.Type.AUTHOR, pubKeyHex)
+ addInt(Tlv.Type.KIND, kind)
+ }.build().toNAddress()
}
companion object {
@@ -63,10 +51,11 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela
if (key.startsWith("naddr")) {
val tlv = Tlv.parse(key.bechToBytes())
- val d = tlv.get(Tlv.Type.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) ?: ""
- val relay = tlv.get(Tlv.Type.RELAY.id)?.get(0)?.toString(Charsets.UTF_8)
- val author = tlv.get(Tlv.Type.AUTHOR.id)?.get(0)?.toHexKey()
- val kind = tlv.get(Tlv.Type.KIND.id)?.get(0)?.let { Tlv.toInt32(it) }
+
+ val d = tlv.firstAsString(Tlv.Type.SPECIAL) ?: ""
+ val relay = tlv.firstAsString(Tlv.Type.RELAY)
+ val author = tlv.firstAsHex(Tlv.Type.AUTHOR)
+ val kind = tlv.firstAsInt(Tlv.Type.KIND)
if (kind != null && author != null) {
return ATag(kind, author, d, relay)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/AdvertisedRelayListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/AdvertisedRelayListEvent.kt
new file mode 100644
index 000000000..5403809fc
--- /dev/null
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/AdvertisedRelayListEvent.kt
@@ -0,0 +1,70 @@
+package com.vitorpamplona.amethyst.service.model
+
+import androidx.compose.runtime.Immutable
+import com.vitorpamplona.amethyst.model.HexKey
+import com.vitorpamplona.amethyst.model.TimeUtils
+import com.vitorpamplona.amethyst.model.toHexKey
+import com.vitorpamplona.amethyst.service.CryptoUtils
+
+@Immutable
+class AdvertisedRelayListEvent(
+ id: HexKey,
+ pubKey: HexKey,
+ createdAt: Long,
+ tags: List>,
+ content: String,
+ sig: HexKey
+) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
+ override fun dTag() = fixedDTag
+ override fun address() = ATag(kind, pubKey, dTag(), null)
+
+ fun relays(): List {
+ return tags.mapNotNull {
+ if (it.size > 1 && it[0] == "r") {
+ val type = when (it.getOrNull(2)) {
+ "read" -> AdvertisedRelayType.READ
+ "write" -> AdvertisedRelayType.WRITE
+ else -> AdvertisedRelayType.BOTH
+ }
+
+ AdvertisedRelayInfo(it[1], type)
+ } else {
+ null
+ }
+ }
+ }
+
+ companion object {
+ const val kind = 10002
+ const val fixedDTag = ""
+
+ fun create(
+ list: List,
+ privateKey: ByteArray,
+ createdAt: Long = TimeUtils.now()
+ ): AdvertisedRelayListEvent {
+ val tags = list.map {
+ if (it.type == AdvertisedRelayType.BOTH) {
+ listOf(it.relayUrl)
+ } else {
+ listOf(it.relayUrl, it.type.code)
+ }
+ }
+ val msg = ""
+ val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
+ val id = generateId(pubKey, createdAt, kind, tags, msg)
+ val sig = CryptoUtils.sign(id, privateKey)
+ return AdvertisedRelayListEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
+ }
+ }
+
+ @Immutable
+ data class AdvertisedRelayInfo(val relayUrl: String, val type: AdvertisedRelayType)
+
+ @Immutable
+ enum class AdvertisedRelayType(val code: String) {
+ BOTH(""),
+ READ("read"),
+ WRITE("write")
+ }
+}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt
index fcb5301c6..0e8d6d69b 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt
@@ -14,6 +14,7 @@ class EventFactory {
sig: String,
lenient: Boolean
) = when (kind) {
+ AdvertisedRelayListEvent.kind -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt
index 7b9c7b301..8ae6115ee 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt
@@ -26,6 +26,8 @@ class FileHeaderEvent(
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
+ fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
+
companion object {
const val kind = 1063
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt
index cf0e386ba..4222be0d8 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt
@@ -29,12 +29,12 @@ class HighlightEvent(
msg: String,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
- ): PollNoteEvent {
+ ): HighlightEvent {
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf>()
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = CryptoUtils.sign(id, privateKey)
- return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
+ return HighlightEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
}
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt
index 39a5b0cb0..937e99b01 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.service.nip19
import android.util.Log
import androidx.compose.runtime.Immutable
-import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.bechToBytes
import com.vitorpamplona.amethyst.service.toNEvent
@@ -82,13 +81,8 @@ object Nip19 {
private fun nprofile(bytes: ByteArray): Return? {
val tlv = Tlv.parse(bytes)
- val hex = tlv.get(Tlv.Type.SPECIAL.id)
- ?.get(0)
- ?.toHexKey() ?: return null
-
- val relay = tlv.get(Tlv.Type.RELAY.id)
- ?.get(0)
- ?.toString(Charsets.UTF_8)
+ val hex = tlv.firstAsHex(Tlv.Type.SPECIAL) ?: return null
+ val relay = tlv.firstAsString(Tlv.Type.RELAY)
return Return(Type.USER, hex, relay)
}
@@ -96,30 +90,16 @@ object Nip19 {
private fun nevent(bytes: ByteArray): Return? {
val tlv = Tlv.parse(bytes)
- val hex = tlv.get(Tlv.Type.SPECIAL.id)
- ?.get(0)
- ?.toHexKey() ?: return null
-
- val relay = tlv.get(Tlv.Type.RELAY.id)
- ?.get(0)
- ?.toString(Charsets.UTF_8)
-
- val author = tlv.get(Tlv.Type.AUTHOR.id)
- ?.get(0)
- ?.toHexKey()
-
- val kind = tlv.get(Tlv.Type.KIND.id)
- ?.get(0)
- ?.let { Tlv.toInt32(it) }
+ val hex = tlv.firstAsHex(Tlv.Type.SPECIAL) ?: return null
+ val relay = tlv.firstAsString(Tlv.Type.RELAY)
+ val author = tlv.firstAsHex(Tlv.Type.AUTHOR)
+ val kind = tlv.firstAsInt(Tlv.Type.KIND.id)
return Return(Type.EVENT, hex, relay, author, kind)
}
private fun nrelay(bytes: ByteArray): Return? {
- val relayUrl = Tlv.parse(bytes)
- .get(Tlv.Type.SPECIAL.id)
- ?.get(0)
- ?.toString(Charsets.UTF_8) ?: return null
+ val relayUrl = Tlv.parse(bytes).firstAsString(Tlv.Type.SPECIAL.id) ?: return null
return Return(Type.RELAY, relayUrl)
}
@@ -127,53 +107,20 @@ object Nip19 {
private fun naddr(bytes: ByteArray): Return? {
val tlv = Tlv.parse(bytes)
- val d = tlv.get(Tlv.Type.SPECIAL.id)
- ?.get(0)
- ?.toString(Charsets.UTF_8) ?: return null
-
- val relay = tlv.get(Tlv.Type.RELAY.id)
- ?.get(0)
- ?.toString(Charsets.UTF_8)
-
- val author = tlv.get(Tlv.Type.AUTHOR.id)
- ?.get(0)
- ?.toHexKey()
-
- val kind = tlv.get(Tlv.Type.KIND.id)
- ?.get(0)
- ?.let { Tlv.toInt32(it) }
+ val d = tlv.firstAsString(Tlv.Type.SPECIAL.id) ?: ""
+ val relay = tlv.firstAsString(Tlv.Type.RELAY.id)
+ val author = tlv.firstAsHex(Tlv.Type.AUTHOR.id) ?: return null
+ val kind = tlv.firstAsInt(Tlv.Type.KIND.id) ?: return null
return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind)
}
public fun createNEvent(idHex: String, author: String?, kind: Int?, relay: String?): String {
- val kind = kind?.toByteArray()
- val author = author?.hexToByteArray()
- val idHex = idHex.hexToByteArray()
- val relay = relay?.toByteArray(Charsets.UTF_8)
-
- var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, idHex.size.toByte()) + idHex
-
- if (relay != null) {
- fullArray = fullArray + byteArrayOf(Tlv.Type.RELAY.id, relay.size.toByte()) + relay
- }
-
- if (author != null) {
- fullArray = fullArray + byteArrayOf(Tlv.Type.AUTHOR.id, author.size.toByte()) + author
- }
-
- if (kind != null) {
- fullArray = fullArray + byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind
- }
-
- return fullArray.toNEvent()
+ return TlvBuilder().apply {
+ addHex(Tlv.Type.SPECIAL, idHex)
+ addStringIfNotNull(Tlv.Type.RELAY, relay)
+ addHexIfNotNull(Tlv.Type.AUTHOR, author)
+ addIntIfNotNull(Tlv.Type.KIND, kind)
+ }.build().toNEvent()
}
}
-
-fun Int.toByteArray(): ByteArray {
- val bytes = ByteArray(4)
- (0..3).forEach {
- bytes[3 - it] = ((this ushr (8 * it)) and 0xFFFF).toByte()
- }
- return bytes
-}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt
index 83bcc0c5e..eca8ad968 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt
@@ -1,9 +1,67 @@
package com.vitorpamplona.amethyst.service.nip19
+import com.vitorpamplona.amethyst.model.HexKey
+import com.vitorpamplona.amethyst.model.hexToByteArray
+import com.vitorpamplona.amethyst.model.toHexKey
+import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
-object Tlv {
+class TlvBuilder() {
+ val outputStream = ByteArrayOutputStream()
+
+ private fun add(type: Byte, byteArray: ByteArray) {
+ outputStream.write(byteArrayOf(type, byteArray.size.toByte()))
+ outputStream.write(byteArray)
+ }
+
+ fun addString(type: Byte, string: String) = add(type, string.toByteArray(Charsets.UTF_8))
+ fun addHex(type: Byte, key: HexKey) = add(type, key.hexToByteArray())
+ fun addInt(type: Byte, data: Int) = add(type, data.toByteArray())
+
+ fun addStringIfNotNull(type: Byte, data: String?) = data?.let { addString(type, it) }
+ fun addHexIfNotNull(type: Byte, data: HexKey?) = data?.let { addHex(type, it) }
+ fun addIntIfNotNull(type: Byte, data: Int?) = data?.let { addInt(type, it) }
+
+ fun addString(type: Tlv.Type, string: String) = addString(type.id, string)
+ fun addHex(type: Tlv.Type, key: HexKey) = addHex(type.id, key)
+ fun addInt(type: Tlv.Type, data: Int) = addInt(type.id, data)
+
+ fun addStringIfNotNull(type: Tlv.Type, data: String?) = addStringIfNotNull(type.id, data)
+ fun addHexIfNotNull(type: Tlv.Type, data: HexKey?) = addHexIfNotNull(type.id, data)
+ fun addIntIfNotNull(type: Tlv.Type, data: Int?) = addIntIfNotNull(type.id, data)
+
+ fun build(): ByteArray {
+ return outputStream.toByteArray()
+ }
+}
+
+fun Int.toByteArray(): ByteArray {
+ val bytes = ByteArray(4)
+ (0..3).forEach {
+ bytes[3 - it] = ((this ushr (8 * it)) and 0xFFFF).toByte()
+ }
+ return bytes
+}
+
+fun ByteArray.toInt32(): Int? {
+ if (size != 4) return null
+ return ByteBuffer.wrap(this, 0, 4).order(ByteOrder.BIG_ENDIAN).int
+}
+
+class Tlv(val data: Map>) {
+ fun asInt(type: Byte) = data[type]?.mapNotNull { it.toInt32() }
+ fun asHex(type: Byte) = data[type]?.map { it.toHexKey().intern() }
+ fun asString(type: Byte) = data[type]?.map { it.toString(Charsets.UTF_8) }
+
+ fun firstAsInt(type: Byte) = data[type]?.firstOrNull()?.toInt32()
+ fun firstAsHex(type: Byte) = data[type]?.firstOrNull()?.toHexKey()?.intern()
+ fun firstAsString(type: Byte) = data[type]?.firstOrNull()?.toString(Charsets.UTF_8)
+
+ fun firstAsInt(type: Type) = firstAsInt(type.id)
+ fun firstAsHex(type: Type) = firstAsHex(type.id)
+ fun firstAsString(type: Type) = firstAsString(type.id)
+
enum class Type(val id: Byte) {
SPECIAL(0),
RELAY(1),
@@ -11,26 +69,24 @@ object Tlv {
KIND(3);
}
- fun toInt32(bytes: ByteArray): Int {
- require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" }
- return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int
- }
+ companion object {
- fun parse(data: ByteArray): Map> {
- val result = mutableMapOf>()
- var rest = data
- while (rest.isNotEmpty()) {
- val t = rest[0]
- val l = rest[1].toUByte().toInt()
- val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
- rest = rest.sliceArray(IntRange(2 + l, rest.size - 1))
- if (v.size < l) continue
+ fun parse(data: ByteArray): Tlv {
+ val result = mutableMapOf>()
+ var rest = data
+ while (rest.isNotEmpty()) {
+ val t = rest[0]
+ val l = rest[1].toUByte().toInt()
+ val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
+ rest = rest.sliceArray(IntRange(2 + l, rest.size - 1))
+ if (v.size < l) continue
- if (!result.containsKey(t)) {
- result[t] = mutableListOf()
+ if (!result.containsKey(t)) {
+ result[t] = mutableListOf()
+ }
+ result[t]?.add(v)
}
- result[t]?.add(v)
+ return Tlv(result)
}
- return result
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt
index d2575683b..f5198a016 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt
@@ -141,6 +141,7 @@ object ImageUploader {
fun NIP98Header(url: String, method: String, body: String): String {
val noteJson = account.createHTTPAuthorization(url, method, body)?.toJson() ?: ""
+
val encodedNIP98Event: String = Base64.getEncoder().encodeToString(noteJson.toByteArray())
return "Nostr " + encodedNIP98Event
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt
index 5a5ba5c5d..0d9ade1c7 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt
@@ -7,12 +7,10 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -24,8 +22,9 @@ import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Surface
+import androidx.compose.material.Scaffold
import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.DeleteSweep
@@ -69,6 +68,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.Size35dp
+import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.CoroutineScope
@@ -76,7 +76,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.lang.Math.round
-@OptIn(ExperimentalLayoutApi::class)
@Composable
fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "", nav: (String) -> Unit) {
val postViewModel: NewRelayListViewModel = viewModel()
@@ -113,58 +112,72 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
}
Dialog(
- onDismissRequest = { onClose() },
- properties = DialogProperties(
- decorFitsSystemWindows = false,
- usePlatformDefaultWidth = false,
- dismissOnClickOutside = false
- )
+ onDismissRequest = onClose,
+ properties = DialogProperties(usePlatformDefaultWidth = false)
) {
- Surface(modifier = Modifier.imePadding()) {
- Column(
- modifier = Modifier.padding(10.dp)
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- CloseButton(onCancel = {
- postViewModel.clear()
- onClose()
- })
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(end = 10.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Spacer(modifier = StdHorzSpacer)
- Button(
- onClick = {
- postViewModel.deleteAll()
- defaultRelays.forEach {
- postViewModel.addRelay(it)
- }
- postViewModel.relays.value.forEach { item ->
- loadRelayInfo(item.url, context, scope) {
- postViewModel.togglePaidRelay(item, it.limitation?.payment_required ?: false)
+ Button(
+ onClick = {
+ postViewModel.deleteAll()
+ defaultRelays.forEach {
+ postViewModel.addRelay(it)
+ }
+ postViewModel.relays.value.forEach { item ->
+ loadRelayInfo(item.url, context, scope) {
+ postViewModel.togglePaidRelay(item, it.limitation?.payment_required ?: false)
+ }
+ }
}
+ ) {
+ Text(stringResource(R.string.default_relays))
}
+
+ PostButton(
+ onPost = {
+ if (PackageUtils.isAmberInstalled(context)) {
+ event = postViewModel.create(false)
+ } else {
+ postViewModel.create(true)
+ onClose()
+ }
+ },
+ true
+ )
}
- ) {
- Text(stringResource(R.string.default_relays))
- }
-
- PostButton(
- onPost = {
- if (PackageUtils.isAmberInstalled(context)) {
- event = postViewModel.create(false)
- } else {
- postViewModel.create(true)
- onClose()
- }
- },
- true
- )
- }
-
- Spacer(modifier = StdVertSpacer)
+ },
+ navigationIcon = {
+ Spacer(modifier = StdHorzSpacer)
+ CloseButton(onCancel = {
+ postViewModel.clear()
+ onClose()
+ })
+ },
+ backgroundColor = MaterialTheme.colors.surface,
+ elevation = 0.dp
+ )
+ }
+ ) { pad ->
+ val scope = rememberCoroutineScope()
+ Column(
+ modifier = Modifier.padding(
+ 16.dp,
+ pad.calculateTopPadding(),
+ 16.dp,
+ pad.calculateBottomPadding()
+ ),
+ verticalArrangement = Arrangement.SpaceAround
+ ) {
Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
LazyColumn(
contentPadding = PaddingValues(
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt
index aa19e1be5..926a82638 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt
@@ -48,7 +48,7 @@ fun SelectTextDialog(text: String, onDismiss: () -> Unit) {
IconButton(
onClick = onDismiss
) {
- ArrowBackIcon(Size24dp)
+ ArrowBackIcon()
}
Text(text = stringResource(R.string.select_text_dialog_top))
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt
index e09fa5cde..afa004ddd 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt
@@ -35,7 +35,7 @@ import kotlinx.collections.immutable.ImmutableList
@Composable
fun TextSpinner(
- label: String,
+ label: String?,
placeholder: String,
options: ImmutableList,
explainers: ImmutableList? = null,
@@ -54,7 +54,7 @@ fun TextSpinner(
value = currentText,
onValueChange = {},
readOnly = true,
- label = { Text(label) },
+ label = { label?.let { Text(it) } },
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt
index 1ebc877db..fc88acdb4 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt
@@ -37,7 +37,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() {
return collection
.asSequence()
- .filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent }
+ .filter { (it.event is FileHeaderEvent && (it.event as FileHeaderEvent).hasUrl()) || it.event is FileStorageHeaderEvent }
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false) || (it.event?.isTaggedGeoHashes(followingGeohashSet) ?: false) }
.filter { isHiddenList || account.isAcceptable(it) }
.filter { it.createdAt()!! <= now }
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt
index 40098c28d..b56582716 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt
@@ -21,7 +21,6 @@ import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Logout
import androidx.compose.material.icons.filled.RadioButtonChecked
import androidx.compose.runtime.Composable
@@ -52,6 +51,7 @@ import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
+import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -108,11 +108,7 @@ fun AccountSwitchBottomSheet(
title = { Text(text = stringResource(R.string.account_switch_add_account_dialog_title)) },
navigationIcon = {
IconButton(onClick = { popupExpanded = false }) {
- Icon(
- imageVector = Icons.Default.ArrowBack,
- contentDescription = stringResource(R.string.back),
- tint = MaterialTheme.colors.onSurface
- )
+ ArrowBackIcon()
}
},
backgroundColor = Color.Transparent,
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt
index f5dcb29a1..af92d985a 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt
@@ -8,22 +8,29 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.AppBarDefaults
+import androidx.compose.material.ContentAlpha
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
+import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.MaterialTheme
+import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.ScaffoldState
+import androidx.compose.material.Surface
import androidx.compose.material.Text
-import androidx.compose.material.TopAppBar
+import androidx.compose.material.contentColorFor
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
@@ -39,20 +46,21 @@ import androidx.compose.ui.Modifier
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
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavHostController
import coil.Coil
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
+import com.vitorpamplona.amethyst.model.ChatroomKey
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
+import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
@@ -75,22 +83,37 @@ 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.AmethystIcon
+import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
+import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.LoadChannel
+import com.vitorpamplona.amethyst.ui.note.LoadUser
+import com.vitorpamplona.amethyst.ui.note.LongCommunityHeader
+import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
import com.vitorpamplona.amethyst.ui.note.SearchIcon
+import com.vitorpamplona.amethyst.ui.note.ShortCommunityHeader
+import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
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.DislayGeoTagHeader
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashActionOptions
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagActionOptions
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRoom
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRoomByAuthor
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongChannelHeader
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongRoomHeader
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.RoomNameOnlyDisplay
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShortChannelHeader
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowVideoStreaming
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
+import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
+import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
+import com.vitorpamplona.amethyst.ui.theme.Size34dp
+import com.vitorpamplona.amethyst.ui.theme.Size40dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -108,7 +131,8 @@ fun AppTopBar(
navEntryState: State,
scaffoldState: ScaffoldState,
accountViewModel: AccountViewModel,
- nav: (String) -> Unit
+ nav: (String) -> Unit,
+ navPopBack: () -> Unit
) {
val currentRoute by remember(navEntryState.value) {
derivedStateOf {
@@ -122,7 +146,7 @@ fun AppTopBar(
}
}
- RenderTopRouteBar(currentRoute, id, followLists, scaffoldState, accountViewModel, nav)
+ RenderTopRouteBar(currentRoute, id, followLists, scaffoldState, accountViewModel, nav, navPopBack)
}
@Composable
@@ -132,67 +156,24 @@ private fun RenderTopRouteBar(
followLists: FollowListViewModel,
scaffoldState: ScaffoldState,
accountViewModel: AccountViewModel,
- nav: (String) -> Unit
+ nav: (String) -> Unit,
+ navPopBack: () -> Unit
) {
when (currentRoute) {
- // 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)
+ Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack)
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.RoomByAuthor.base -> LoadRoomByAuthor(authorPubKeyHex = id, accountViewModel) {
- if (it != null) {
- ChatroomHeader(
- room = it,
- modifier = Modifier.padding(vertical = 4.dp, horizontal = 11.dp),
- accountViewModel = accountViewModel,
- nav = nav
- )
- } else {
- Spacer(BottomTopHeight)
- }
- }
- Route.Room.base -> LoadRoom(roomId = id, accountViewModel) {
- if (it != null) {
- ChatroomHeader(
- room = 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)
+ Route.Channel.base -> ChannelTopBar(id, accountViewModel, nav, navPopBack)
+ Route.RoomByAuthor.base -> RoomByAuthorTopBar(id, accountViewModel, nav, navPopBack)
+ Route.Room.base -> RoomTopBar(id, accountViewModel, nav, navPopBack)
+ Route.Community.base -> CommunityTopBar(id, accountViewModel, nav, navPopBack)
+ Route.Hashtag.base -> HashTagTopBar(id, accountViewModel, navPopBack)
+ Route.Geohash.base -> GeoHashTopBar(id, accountViewModel, navPopBack)
else -> MainTopBar(scaffoldState, accountViewModel, nav)
}
} else {
@@ -202,13 +183,180 @@ private fun RenderTopRouteBar(
}
}
+@Composable
+private fun GeoHashTopBar(
+ tag: String,
+ accountViewModel: AccountViewModel,
+ navPopBack: () -> Unit
+) {
+ FlexibleTopBarWithBackButton(
+ title = {
+ DislayGeoTagHeader(tag, remember { Modifier.weight(1f) })
+ GeoHashActionOptions(tag, accountViewModel)
+ },
+ popBack = navPopBack
+ )
+}
+
+@Composable
+private fun HashTagTopBar(
+ tag: String,
+ accountViewModel: AccountViewModel,
+ navPopBack: () -> Unit
+) {
+ FlexibleTopBarWithBackButton(
+ title = {
+ Text(
+ remember(tag) { "#$tag" },
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.weight(1f)
+ )
+
+ HashtagActionOptions(tag, accountViewModel)
+ },
+ popBack = navPopBack
+ )
+}
+
+@Composable
+private fun CommunityTopBar(
+ id: String,
+ accountViewModel: AccountViewModel,
+ nav: (String) -> Unit,
+ navPopBack: () -> Unit
+) {
+ LoadAddressableNote(aTagHex = id) { baseNote ->
+ if (baseNote != null) {
+ FlexibleTopBarWithBackButton(
+ title = {
+ ShortCommunityHeader(baseNote, fontWeight = FontWeight.Medium, accountViewModel, nav)
+ },
+ extendableRow = {
+ LongCommunityHeader(baseNote, accountViewModel, nav)
+ },
+ popBack = navPopBack
+ )
+ } else {
+ Spacer(BottomTopHeight)
+ }
+ }
+}
+
+@Composable
+private fun RoomTopBar(
+ id: String,
+ accountViewModel: AccountViewModel,
+ nav: (String) -> Unit,
+ navPopBack: () -> Unit
+) {
+ LoadRoom(roomId = id, accountViewModel) { room ->
+ if (room != null) {
+ RenderRoomTopBar(room, accountViewModel, nav, navPopBack)
+ } else {
+ Spacer(BottomTopHeight)
+ }
+ }
+}
+
+@Composable
+private fun RoomByAuthorTopBar(
+ id: String,
+ accountViewModel: AccountViewModel,
+ nav: (String) -> Unit,
+ navPopBack: () -> Unit
+) {
+ LoadRoomByAuthor(authorPubKeyHex = id, accountViewModel) { room ->
+ if (room != null) {
+ RenderRoomTopBar(room, accountViewModel, nav, navPopBack)
+ } else {
+ Spacer(BottomTopHeight)
+ }
+ }
+}
+
+@Composable
+private fun RenderRoomTopBar(
+ room: ChatroomKey,
+ accountViewModel: AccountViewModel,
+ nav: (String) -> Unit,
+ navPopBack: () -> Unit
+) {
+ if (room.users.size == 1) {
+ FlexibleTopBarWithBackButton(
+ title = {
+ LoadUser(baseUserHex = room.users.first()) { baseUser ->
+ if (baseUser != null) {
+ ClickableUserPicture(
+ baseUser = baseUser,
+ accountViewModel = accountViewModel,
+ size = Size34dp
+ )
+
+ Spacer(modifier = DoubleHorzSpacer)
+
+ UsernameDisplay(baseUser, Modifier.weight(1f), fontWeight = FontWeight.Medium)
+ }
+ }
+ },
+ popBack = navPopBack
+ )
+ } else {
+ FlexibleTopBarWithBackButton(
+ title = {
+ NonClickableUserPictures(
+ users = room.users,
+ accountViewModel = accountViewModel,
+ size = Size34dp
+ )
+
+ RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp).weight(1f), fontWeight = FontWeight.Medium, accountViewModel.userProfile())
+ },
+ extendableRow = {
+ LongRoomHeader(room, accountViewModel, nav)
+ },
+ popBack = navPopBack
+ )
+ }
+}
+
+@Composable
+private fun ChannelTopBar(
+ id: String,
+ accountViewModel: AccountViewModel,
+ nav: (String) -> Unit,
+ navPopBack: () -> Unit
+) {
+ LoadChannel(baseChannelHex = id) { baseChannel ->
+ FlexibleTopBarWithBackButton(
+ prefixRow = {
+ if (baseChannel is LiveActivitiesChannel) {
+ ShowVideoStreaming(baseChannel, accountViewModel)
+ }
+ },
+ title = {
+ ShortChannelHeader(
+ baseChannel = baseChannel,
+ accountViewModel = accountViewModel,
+ fontWeight = FontWeight.Medium,
+ nav = nav,
+ showFlag = true
+ )
+ },
+ extendableRow = {
+ LongChannelHeader(baseChannel, accountViewModel, nav)
+ },
+ popBack = navPopBack
+ )
+ }
+}
+
@Composable
fun NoTopBar() {
}
@Composable
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
- GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
+ GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS)
FollowList(
@@ -223,7 +371,7 @@ fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState
@Composable
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
- GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
+ GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS)
FollowList(
@@ -238,7 +386,7 @@ fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, a
@Composable
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
- GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
+ GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS)
FollowList(
@@ -253,7 +401,7 @@ fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: Scaffold
@Composable
fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
- GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
+ GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel ->
val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS)
FollowList(
@@ -268,18 +416,23 @@ fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldSta
@Composable
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
- GenericTopBar(scaffoldState, accountViewModel, nav) {
- AmethystIcon()
+ GenericMainTopBar(scaffoldState, accountViewModel, nav) {
+ AmethystClickableIcon()
}
}
@OptIn(coil.annotation.ExperimentalCoilApi::class)
@Composable
-fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit, content: @Composable (AccountViewModel) -> Unit) {
+fun GenericMainTopBar(
+ scaffoldState: ScaffoldState,
+ accountViewModel: AccountViewModel,
+ nav: (String) -> Unit,
+ content: @Composable (AccountViewModel) -> Unit
+) {
val coroutineScope = rememberCoroutineScope()
Column(modifier = BottomTopHeight) {
- TopAppBar(
+ MyTopAppBar(
elevation = 0.dp,
backgroundColor = MaterialTheme.colors.surface,
title = {
@@ -291,8 +444,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
Column(
modifier = Modifier
.fillMaxWidth()
- .fillMaxHeight()
- .padding(start = 0.dp, end = 20.dp),
+ .fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
@@ -488,25 +640,17 @@ fun SimpleTextSpinner(
}
@Composable
-fun TopBarWithBackButton(navController: NavHostController) {
- Column() {
- TopAppBar(
+fun TopBarWithBackButton(caption: String, popBack: () -> Unit) {
+ Column(modifier = BottomTopHeight) {
+ MyTopAppBar(
elevation = 0.dp,
- backgroundColor = Color(0xFFFFFF),
- title = {},
+ title = { Text(caption) },
navigationIcon = {
IconButton(
- onClick = {
- navController.popBackStack()
- },
+ onClick = popBack,
modifier = Modifier
) {
- Icon(
- imageVector = Icons.Filled.ArrowBack,
- null,
- modifier = Modifier.size(28.dp),
- tint = MaterialTheme.colors.primary
- )
+ ArrowBackIcon()
}
},
actions = {}
@@ -516,7 +660,34 @@ fun TopBarWithBackButton(navController: NavHostController) {
}
@Composable
-fun AmethystIcon() {
+fun FlexibleTopBarWithBackButton(
+ prefixRow: (@Composable () -> Unit)? = null,
+ title: @Composable RowScope.() -> Unit,
+ extendableRow: (@Composable () -> Unit)? = null,
+ popBack: () -> Unit
+) {
+ Column() {
+ MyExtensibleTopAppBar(
+ elevation = 0.dp,
+ prefixRow = prefixRow,
+ title = title,
+ extendableRow = extendableRow,
+ navigationIcon = {
+ IconButton(
+ onClick = popBack,
+ modifier = Modifier
+ ) {
+ ArrowBackIcon()
+ }
+ },
+ actions = {}
+ )
+ Divider(thickness = 0.25.dp)
+ }
+}
+
+@Composable
+fun AmethystClickableIcon() {
val context = LocalContext.current
IconButton(
@@ -524,12 +695,7 @@ fun AmethystIcon() {
debugState(context)
}
) {
- Icon(
- painter = painterResource(R.drawable.amethyst),
- null,
- modifier = Modifier.size(40.dp),
- tint = Color.Unspecified
- )
+ AmethystIcon(Size40dp)
}
}
@@ -579,3 +745,151 @@ fun debugState(context: Context) {
Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value.size} elements ")
}
}
+
+@Composable
+fun MyTopAppBar(
+ title: @Composable RowScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ navigationIcon: @Composable (() -> Unit)? = null,
+ actions: @Composable RowScope.() -> Unit = {},
+ backgroundColor: Color = MaterialTheme.colors.surface,
+ contentColor: Color = contentColorFor(backgroundColor),
+ elevation: Dp = AppBarDefaults.TopAppBarElevation
+) {
+ Surface(
+ contentColor = contentColor,
+ elevation = elevation,
+ modifier = modifier
+ ) {
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(AppBarDefaults.ContentPadding),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (navigationIcon == null) {
+ Spacer(TitleInsetWithoutIcon)
+ } else {
+ Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) {
+ CompositionLocalProvider(
+ LocalContentAlpha provides ContentAlpha.high,
+ content = navigationIcon
+ )
+ }
+ }
+
+ Row(
+ Modifier.weight(1f),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ ProvideTextStyle(MaterialTheme.typography.h6) {
+ title()
+ }
+ }
+
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+ Row(
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.CenterVertically,
+ content = actions
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun MyExtensibleTopAppBar(
+ prefixRow: (@Composable () -> Unit)? = null,
+ title: @Composable RowScope.() -> Unit,
+ extendableRow: (@Composable () -> Unit)? = null,
+ modifier: Modifier = Modifier,
+ navigationIcon: @Composable (() -> Unit)? = null,
+ actions: @Composable RowScope.() -> Unit = {},
+ backgroundColor: Color = MaterialTheme.colors.surface,
+ contentColor: Color = contentColorFor(backgroundColor),
+ elevation: Dp = AppBarDefaults.TopAppBarElevation
+) {
+ val expanded = remember { mutableStateOf(false) }
+
+ Surface(
+ color = backgroundColor,
+ contentColor = contentColor,
+ elevation = elevation,
+ modifier = modifier.clickable {
+ expanded.value = !expanded.value
+ }
+ ) {
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+ Column(Modifier.fillMaxWidth()) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(AppBarDefaults.ContentPadding),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (navigationIcon == null) {
+ Spacer(TitleInsetWithoutIcon)
+ } else {
+ Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) {
+ CompositionLocalProvider(
+ LocalContentAlpha provides ContentAlpha.high,
+ content = navigationIcon
+ )
+ }
+ }
+
+ Row(
+ Modifier.weight(1f),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ ProvideTextStyle(MaterialTheme.typography.h6) {
+ title()
+ }
+ }
+
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+ Row(
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.CenterVertically,
+ content = actions
+ )
+ }
+ }
+
+ if (expanded.value && extendableRow != null) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(bottom = Size10dp),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ extendableRow()
+ }
+ }
+ }
+
+ if (prefixRow != null) {
+ prefixRow()
+ }
+ }
+ }
+ }
+}
+
+private val AppBarHeight = 50.dp
+
+// TODO: this should probably be part of the touch target of the start and end icons, clarify this
+private val AppBarHorizontalPadding = 4.dp
+
+// Start inset for the title when there is no navigation icon provided
+private val TitleInsetWithoutIcon = Modifier.width(16.dp - AppBarHorizontalPadding)
+
+// Start inset for the title when there is a navigation icon provided
+private val TitleIconModifier = Modifier.width(48.dp - AppBarHorizontalPadding)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt
index dba6791bf..1e72fd759 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt
@@ -35,6 +35,16 @@ import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.subtleButton
+@Composable
+fun AmethystIcon(iconSize: Dp) {
+ Icon(
+ painter = painterResource(R.drawable.amethyst),
+ null,
+ modifier = Modifier.size(iconSize),
+ tint = Color.Unspecified
+ )
+}
+
@Composable
fun FollowingIcon(iconSize: Dp) {
Icon(
@@ -46,12 +56,11 @@ fun FollowingIcon(iconSize: Dp) {
}
@Composable
-fun ArrowBackIcon(iconSize: Dp) {
+fun ArrowBackIcon() {
Icon(
imageVector = Icons.Default.ArrowBack,
- contentDescription = null,
- modifier = remember(iconSize) { Modifier.size(iconSize) },
- tint = MaterialTheme.colors.primary
+ contentDescription = stringResource(R.string.back),
+ tint = MaterialTheme.colors.onSurface
)
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt
index 8b1ea55fe..209654eb0 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt
@@ -527,7 +527,11 @@ fun CommunityHeader(
}
}
) {
- ShortCommunityHeader(baseNote, expanded, accountViewModel, nav)
+ ShortCommunityHeader(
+ baseNote = baseNote,
+ accountViewModel = accountViewModel,
+ nav = nav
+ )
if (expanded.value) {
LongCommunityHeader(baseNote, accountViewModel, nav)
@@ -685,7 +689,7 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView
}
@Composable
-fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
+fun ShortCommunityHeader(baseNote: AddressableNote, fontWeight: FontWeight = FontWeight.Bold, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteState by baseNote.live().metadata.observeAsState()
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
@@ -710,27 +714,11 @@ fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState,
accountViewModel: AccountViewModel,
+ fontWeight: FontWeight = FontWeight.Bold,
nav: (String) -> Unit,
showFlag: Boolean
) {
@@ -695,27 +699,11 @@ private fun ShortChannelHeader(
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = remember(channelState) { channel.toBestDisplayName() },
- fontWeight = FontWeight.Bold,
+ fontWeight = fontWeight,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
-
- val summary = remember(channelState) {
- channel.summary()?.ifBlank { null }
- }
-
- if (summary != null && !expanded.value) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = summary,
- color = MaterialTheme.colors.placeholderText,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- fontSize = 12.sp
- )
- }
- }
}
Row(
@@ -735,7 +723,7 @@ private fun ShortChannelHeader(
}
@Composable
-private fun LongChannelHeader(
+fun LongChannelHeader(
baseChannel: Channel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -969,6 +957,7 @@ fun LiveFlag() {
text = stringResource(id = R.string.live_stream_live_tag),
color = Color.White,
fontWeight = FontWeight.Bold,
+ fontSize = 16.sp,
modifier = remember {
Modifier
.clip(SmallBorder)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt
index ebe79b276..f424a1a09 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt
@@ -531,7 +531,7 @@ fun GroupChatroomHeader(
)
Column(modifier = Modifier.padding(start = 10.dp)) {
- RoomNameOnlyDisplay(room, Modifier, accountViewModel.userProfile())
+ RoomNameOnlyDisplay(room, Modifier, FontWeight.Bold, accountViewModel.userProfile())
DisplayUserSetAsSubject(room, FontWeight.Normal)
}
}
@@ -714,14 +714,14 @@ fun LongRoomHeader(room: ChatroomKey, accountViewModel: AccountViewModel, nav: (
}
@Composable
-fun RoomNameOnlyDisplay(room: ChatroomKey, modifier: Modifier, loggedInUser: User) {
+fun RoomNameOnlyDisplay(room: ChatroomKey, modifier: Modifier, fontWeight: FontWeight = FontWeight.Bold, loggedInUser: User) {
val roomSubject by loggedInUser.live().messages.map {
it.user.privateChatrooms[room]?.subject
}.distinctUntilChanged().observeAsState(loggedInUser.privateChatrooms[room]?.subject)
Crossfade(targetState = roomSubject, modifier) {
if (it != null && it.isNotBlank()) {
- DisplayRoomSubject(it)
+ DisplayRoomSubject(it, fontWeight)
}
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt
index 887c33ba2..8d0db4e3c 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt
@@ -119,7 +119,7 @@ fun GeoHashHeader(tag: String, modifier: Modifier = StdPadding, account: Account
) {
DislayGeoTagHeader(tag, remember { Modifier.weight(1f) })
- HashtagActionOptions(tag, account)
+ GeoHashActionOptions(tag, account)
}
}
@@ -154,7 +154,7 @@ fun DislayGeoTagHeader(geohash: String, modifier: Modifier) {
}
@Composable
-private fun HashtagActionOptions(
+fun GeoHashActionOptions(
tag: String,
accountViewModel: AccountViewModel
) {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt
index c17146046..f9fa24f19 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt
@@ -137,7 +137,7 @@ fun HashtagHeader(tag: String, modifier: Modifier = StdPadding, account: Account
}
@Composable
-private fun HashtagActionOptions(
+fun HashtagActionOptions(
tag: String,
accountViewModel: AccountViewModel
) {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt
index c1f573b3b..ce9cce988 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt
@@ -97,6 +97,13 @@ fun MainScreen(
}
}
+ val navPopBack = remember(navController) {
+ {
+ navController.popBackStack()
+ Unit
+ }
+ }
+
val followLists: FollowListViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "FollowListViewModel",
factory = FollowListViewModel.Factory(accountViewModel.account)
@@ -203,7 +210,7 @@ fun MainScreen(
AppBottomBar(accountViewModel, navState, navBottomRow)
},
topBar = {
- AppTopBar(followLists, navState, scaffoldState, accountViewModel, nav = nav)
+ AppTopBar(followLists, navState, scaffoldState, accountViewModel, nav = nav, navPopBack)
},
drawerContent = {
DrawerContent(nav, scaffoldState, sheetState, accountViewModel)
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt
index 5f2c2e59d..072d77be5 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt
@@ -19,7 +19,6 @@ import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Block
import androidx.compose.material.icons.filled.Report
import androidx.compose.runtime.Composable
@@ -41,6 +40,7 @@ import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ReportEvent
+import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.theme.WarningColor
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
@@ -70,11 +70,7 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss:
title = { Text(text = stringResource(id = R.string.report_dialog_title)) },
navigationIcon = {
IconButton(onClick = onDismiss) {
- Icon(
- imageVector = Icons.Default.ArrowBack,
- contentDescription = stringResource(R.string.back),
- tint = MaterialTheme.colors.onSurface
- )
+ ArrowBackIcon()
}
},
backgroundColor = MaterialTheme.colors.surface,
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt
index c7108ae65..e7e18bc5a 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -15,6 +16,7 @@ import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExposedDropdownMenuBox
import androidx.compose.material.ExposedDropdownMenuDefaults
+import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
@@ -26,10 +28,12 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.os.LocaleListCompat
@@ -39,7 +43,10 @@ import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.parseConnectivityType
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
-import com.vitorpamplona.amethyst.ui.theme.StdPadding
+import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
+import com.vitorpamplona.amethyst.ui.theme.Size10dp
+import com.vitorpamplona.amethyst.ui.theme.Size20dp
+import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -108,16 +115,18 @@ fun SettingsScreen(
stringResource(ConnectivityType.WIFI_ONLY.reourceId),
stringResource(ConnectivityType.NEVER.reourceId)
)
- val settings = accountViewModel.account.settings
- val index = settings.automaticallyShowImages.screenCode
- val videoIndex = settings.automaticallyStartPlayback.screenCode
- val linkIndex = settings.automaticallyShowUrlPreview.screenCode
val themeItens = persistentListOf(
stringResource(R.string.system),
stringResource(R.string.light),
stringResource(R.string.dark)
)
+
+ val settings = accountViewModel.account.settings
+ val showImagesIndex = settings.automaticallyShowImages.screenCode
+ val videoIndex = settings.automaticallyStartPlayback.screenCode
+ val linkIndex = settings.automaticallyShowUrlPreview.screenCode
+
val themeIndex = themeViewModel.theme.value ?: 0
val context = LocalContext.current
@@ -127,123 +136,132 @@ fun SettingsScreen(
val languageIndex = getLanguageIndex(languageEntries)
Column(
- StdPadding
+ Modifier
+ .padding(top = Size10dp, start = Size20dp, end = Size20dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
- Section(stringResource(R.string.application_preferences))
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth()
+ SettingsRow(
+ R.string.language,
+ R.string.language_description,
+ languageList,
+ languageIndex
) {
- TextSpinner(
- label = stringResource(R.string.language),
- placeholder = languageList[languageIndex],
- options = languageList,
- onSelect = {
- GlobalScope.launch(Dispatchers.Main) {
- val job = scope.launch(Dispatchers.IO) {
- val locale = languageEntries[languageList[it]]
- accountViewModel.account.settings.preferredLanguage = locale
- LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
- }
- job.join()
- val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]])
- AppCompatDelegate.setApplicationLocales(appLocale)
- }
- },
- modifier = Modifier
- .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
- .weight(1f)
+ GlobalScope.launch(Dispatchers.Main) {
+ val job = scope.launch(Dispatchers.IO) {
+ val locale = languageEntries[languageList[it]]
+ accountViewModel.account.settings.preferredLanguage = locale
+ LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
+ }
+ job.join()
+ val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]])
+ AppCompatDelegate.setApplicationLocales(appLocale)
+ }
+ }
+
+ Spacer(modifier = HalfVertSpacer)
+
+ SettingsRow(
+ R.string.theme,
+ R.string.theme_description,
+ themeItens,
+ themeIndex
+ ) {
+ themeViewModel.onChange(it)
+ scope.launch(Dispatchers.IO) {
+ LocalPreferences.updateTheme(it)
+ }
+ }
+
+ Spacer(modifier = HalfVertSpacer)
+
+ SettingsRow(
+ R.string.automatically_load_images_gifs,
+ R.string.automatically_load_images_gifs_description,
+ selectedItens,
+ showImagesIndex
+ ) {
+ val automaticallyShowImages = parseConnectivityType(it)
+
+ scope.launch(Dispatchers.IO) {
+ accountViewModel.updateAutomaticallyShowImages(automaticallyShowImages)
+ LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
+ }
+ }
+
+ Spacer(modifier = HalfVertSpacer)
+
+ SettingsRow(
+ R.string.automatically_play_videos,
+ R.string.automatically_play_videos_description,
+ selectedItens,
+ videoIndex
+ ) {
+ val automaticallyStartPlayback = parseConnectivityType(it)
+
+ scope.launch(Dispatchers.IO) {
+ accountViewModel.updateAutomaticallyStartPlayback(automaticallyStartPlayback)
+ LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
+ }
+ }
+
+ Spacer(modifier = HalfVertSpacer)
+
+ SettingsRow(
+ R.string.automatically_show_url_preview,
+ R.string.automatically_show_url_preview_description,
+ selectedItens,
+ linkIndex
+ ) {
+ val automaticallyShowUrlPreview = parseConnectivityType(it)
+
+ scope.launch(Dispatchers.IO) {
+ accountViewModel.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview)
+ LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
+ }
+ }
+ }
+}
+
+@Composable
+fun SettingsRow(
+ name: Int,
+ description: Int,
+ selectedItens: ImmutableList,
+ selectedIndex: Int,
+ onSelect: (Int) -> Unit
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(
+ modifier = Modifier.weight(2.0f),
+ verticalArrangement = Arrangement.spacedBy(3.dp)
+ ) {
+ Text(
+ text = stringResource(name),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = stringResource(description),
+ style = MaterialTheme.typography.caption,
+ color = Color.Gray,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
)
}
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth()
- ) {
- TextSpinner(
- label = stringResource(R.string.theme),
- placeholder = themeItens[themeIndex],
- options = themeItens,
- onSelect = {
- themeViewModel.onChange(it)
- scope.launch(Dispatchers.IO) {
- LocalPreferences.updateTheme(it)
- }
- },
- modifier = Modifier
- .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
- .weight(1f)
- )
- }
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth()
- ) {
- TextSpinner(
- label = stringResource(R.string.automatically_load_images_gifs),
- placeholder = selectedItens[index],
- options = selectedItens,
- onSelect = {
- val automaticallyShowImages = parseConnectivityType(it)
-
- scope.launch(Dispatchers.IO) {
- accountViewModel.updateAutomaticallyShowImages(automaticallyShowImages)
- LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
- }
- },
- modifier = Modifier
- .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
- .weight(1f)
- )
- }
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth()
- ) {
- TextSpinner(
- label = stringResource(R.string.automatically_play_videos),
- placeholder = selectedItens[videoIndex],
- options = selectedItens,
- onSelect = {
- val automaticallyStartPlayback = parseConnectivityType(it)
-
- scope.launch(Dispatchers.IO) {
- accountViewModel.updateAutomaticallyStartPlayback(automaticallyStartPlayback)
- LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
- }
- },
- modifier = Modifier
- .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
- .weight(1f)
- )
- }
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth()
- ) {
- TextSpinner(
- label = stringResource(R.string.automatically_show_url_preview),
- placeholder = selectedItens[linkIndex],
- options = selectedItens,
- onSelect = {
- val automaticallyShowUrlPreview = parseConnectivityType(it)
-
- scope.launch(Dispatchers.IO) {
- accountViewModel.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview)
- LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
- }
- },
- modifier = Modifier
- .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
- .weight(1f)
- )
- }
+ TextSpinner(
+ label = "",
+ placeholder = selectedItens[selectedIndex],
+ options = selectedItens,
+ onSelect = onSelect,
+ modifier = Modifier
+ .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
+ .weight(1f)
+ )
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt
index 4123cf550..e61c0de51 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt
@@ -62,6 +62,7 @@ val Size25dp = 25.dp
val Size30dp = 30.dp
val Size34dp = 34.dp
val Size35dp = 35.dp
+val Size40dp = 40.dp
val Size55dp = 55.dp
val Size75dp = 75.dp
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index a5aa1b489..3dd6ac96a 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -500,4 +500,26 @@
Přidá Geohash vaší polohy do příspěvku. Veřejnost bude vědět, že se nacházíte do 5 km od aktuální polohy
Přidat varování o citlivém obsahu před zobrazením vašeho obsahu. Toto je ideální pro obsah NSFW (nebezpečné pro práci) nebo obsah, který někteří lidé mohou považovat za urážlivý nebo znepokojující
+
+ Nová funkce
+ Aktivace tohoto režimu vyžaduje od Amethystu odeslání zprávy NIP-24 (GiftWrapped, Zapečetěné přímé a skupinové zprávy). NIP-24 je nový a většina klientů ho zatím neimplementovala. Ujistěte se, že příjemce používá kompatibilního klienta.
+ Aktivovat
+
+ Veřejné
+ Soukromé
+ Pro
+ Předmět
+ Téma konverzace
+ "@Uživatel1, @Uživatel2, @Uživatel3"
+
+ Členové této skupiny
+ Vysvětlení členům
+ Změna názvu pro nové cíle.
+
+ Pro rozhraní aplikace
+ Tmavé, světlé nebo systémové téma
+ Automaticky načítat obrázky a GIFy
+ Automaticky přehrávat videa a GIFy
+ Zobrazit náhledy URL
+ Kdy načíst obrázek
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index e8462631e..e22c6f1b5 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -509,4 +509,26 @@ anz der Bedingungen ist erforderlich
Fügt dem Beitrag einen Geohash Ihres Standorts hinzu. Die Öffentlichkeit wird wissen, dass Sie sich innerhalb von 5 km (3 mi) vom aktuellen Standort befinden
Fügt eine Warnung für sensiblen Inhalt hinzu, bevor Ihr Inhalt angezeigt wird. Dies ist ideal für NSFW-Inhalte (nicht sicher für die Arbeit) oder Inhalte, die manche Menschen als anstößig oder verstörend empfinden könnten
+
+ Neues Feature
+ Um diesen Modus zu aktivieren, muss Amethyst eine NIP-24-Nachricht senden (GiftWrapped, Versiegelte Direkt- und Gruppennachrichten). NIP-24 ist neu und die meisten Clients haben es noch nicht implementiert. Stellen Sie sicher, dass der Empfänger einen kompatiblen Client verwendet.
+ Aktivieren
+
+ Öffentlich
+ Privat
+ An
+ Betreff
+ Gesprächsthema
+ "@Benutzer1, @Benutzer2, @Benutzer3"
+
+ Mitglieder dieser Gruppe
+ Erklärung an Mitglieder
+ Ändern des Namens für die neuen Ziele.
+
+ Für die App-Benutzeroberfläche
+ Dunkles, helles oder Systemdesign
+ Bilder und GIFs automatisch laden
+ Videos und GIFs automatisch abspielen
+ URL-Vorschauen anzeigen
+ Wann Bilder geladen werden sollen
\ No newline at end of file
diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml
index 16820baec..0300e5e1c 100644
--- a/app/src/main/res/values-sv-rSE/strings.xml
+++ b/app/src/main/res/values-sv-rSE/strings.xml
@@ -497,6 +497,28 @@
Lägger till en Geohash av din plats i inlägget. Allmänheten kommer att veta att du befinner dig inom 5 km från nuvarande plats
Lägger till en varning för känsligt innehåll innan ditt innehåll visas. Detta är idealiskt för NSFW-innehåll (inte säkert för arbete) eller innehåll som vissa personer kan uppleva som stötande eller störande
+
+ Ny Funktion
+ För att aktivera denna funktion kräver det att Amethyst skickar ett NIP-24 meddelande (GiftWrapped, Förseglade Direkta och Gruppmeddelanden). NIP-24 är nytt och de flesta klienter har ännu inte implementerat det. Se till att mottagaren använder en kompatibel klient.
+ Aktivera
+
+ Publik
+ Privat
+ Till
+ Ämne
+ Samtalsämne
+ "@Användare1, @Användare2, @Användare3"
+
+ Medlemmar i denna grupp
+ Förklaring till medlemmar
+ Ändra namnet för de nya målen.
+
+ För appens gränssnitt
+ Mörkt, Ljust eller Systemtema
+ Ladda automatiskt bilder och GIFs
+ Spela upp videor och GIFs automatiskt
+ Visa förhandsgranskning av URL
+ När bilder ska laddas
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 908fce70b..b29f806c6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -493,12 +493,12 @@
System
Light
Dark
- Application preferences
+ Application Preferences
Language
Theme
- Automatically load images/gifs
- Automatically play videos
- Automatically show url preview
+ Image Preview
+ Video Playback
+ URL Preview
Load Image
Spammers
@@ -536,5 +536,13 @@
Members of this group
Explanation to members
Changing the name for the new goals.
+
Paste from clipboard
+
+ For the App\'s Interface
+ Dark, Light or System theme
+ Automatically load images and GIFs
+ Automatically plays videos and GIFs
+ Show URL previews
+ When to load images
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
index a407a07dd..64d933c4f 100644
--- a/app/src/main/res/xml/locales_config.xml
+++ b/app/src/main/res/xml/locales_config.xml
@@ -4,10 +4,13 @@
+
+
+
@@ -18,7 +21,4 @@
-
-
-
diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt
new file mode 100644
index 000000000..cf847a33d
--- /dev/null
+++ b/app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt
@@ -0,0 +1,40 @@
+package com.vitorpamplona.amethyst.service
+
+import com.vitorpamplona.amethyst.service.nip19.toByteArray
+import com.vitorpamplona.amethyst.service.nip19.toInt32
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class TlvIntegerTest {
+ fun to_int_32_length_smaller_than_4() {
+ Assert.assertNull(byteArrayOfInts(1, 2, 3).toInt32())
+ }
+
+ fun to_int_32_length_bigger_than_4() {
+ Assert.assertNull(byteArrayOfInts(1, 2, 3, 4, 5).toInt32())
+ }
+
+ @Test()
+ fun to_int_32_length_4() {
+ val actual = byteArrayOfInts(1, 2, 3, 4).toInt32()
+
+ assertEquals(16909060, actual)
+ }
+
+ @Test()
+ fun backAndForth() {
+ assertEquals(234, 234.toByteArray().toInt32())
+ assertEquals(1, 1.toByteArray().toInt32())
+ assertEquals(0, 0.toByteArray().toInt32())
+ assertEquals(1000, 1000.toByteArray().toInt32())
+
+ assertEquals(-234, (-234).toByteArray().toInt32())
+ assertEquals(-1, (-1).toByteArray().toInt32())
+ assertEquals(-0, (-0).toByteArray().toInt32())
+ assertEquals(-1000, (-1000).toByteArray().toInt32())
+ }
+
+ private fun byteArrayOfInts(vararg ints: Int) =
+ ByteArray(ints.size) { pos -> ints[pos].toByte() }
+}
diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt
deleted file mode 100644
index 9f9087036..000000000
--- a/app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.vitorpamplona.amethyst.service
-
-import com.vitorpamplona.amethyst.service.nip19.Tlv
-import org.junit.Assert
-import org.junit.Ignore
-import org.junit.Test
-
-class TlvTest {
-
- @Test(expected = IllegalArgumentException::class)
- fun to_int_32_length_smaller_than_4() {
- Tlv.toInt32(byteArrayOfInts(1, 2, 3))
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun to_int_32_length_bigger_than_4() {
- Tlv.toInt32(byteArrayOfInts(1, 2, 3, 4, 5))
- }
-
- @Test()
- fun to_int_32_length_4() {
- val actual = Tlv.toInt32(byteArrayOfInts(1, 2, 3, 4))
-
- Assert.assertEquals(16909060, actual)
- }
-
- @Ignore("Test not implemented yet")
- @Test()
- fun parse_TLV() {
- // TODO: I don't know how to test this (?)
- }
-
- private fun byteArrayOfInts(vararg ints: Int) =
- ByteArray(ints.size) { pos -> ints[pos].toByte() }
-}