Merge branch 'main' into amber
This commit is contained in:
Generated
-3
@@ -1,3 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
Generated
-1
@@ -1 +0,0 @@
|
|||||||
Amethyst
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
<option name="previewFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
@@ -75,10 +75,13 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
|||||||
- [x] Relay Pages (NIP-11)
|
- [x] Relay Pages (NIP-11)
|
||||||
- [x] HTTP Auth (NIP-98)
|
- [x] HTTP Auth (NIP-98)
|
||||||
- [x] Zapraiser (NIP-TBD)
|
- [x] Zapraiser (NIP-TBD)
|
||||||
- [x] Moderated Communities (NIP-172)
|
- [x] Moderated Communities (NIP-72)
|
||||||
- [x] Emoji Packs (Kind:30030)
|
- [x] Emoji Packs (Kind:30030)
|
||||||
- [x] Personal Emoji Lists (Kind:10030)
|
- [x] Personal Emoji Lists (Kind:10030)
|
||||||
- [x] Classifieds (Kind:30403)
|
- [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)
|
- [ ] Marketplace (NIP-15)
|
||||||
- [ ] Image/Video Capture in the app
|
- [ ] Image/Video Capture in the app
|
||||||
- [ ] Local Database
|
- [ ] Local Database
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
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'
|
id 'com.google.gms.google-services'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ dependencies {
|
|||||||
playImplementation 'com.google.mlkit:translate:17.0.1'
|
playImplementation 'com.google.mlkit:translate:17.0.1'
|
||||||
|
|
||||||
// PushNotifications
|
// 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'
|
playImplementation 'com.google.firebase:firebase-messaging-ktx'
|
||||||
|
|
||||||
// Charts
|
// Charts
|
||||||
@@ -204,7 +204,7 @@ dependencies {
|
|||||||
implementation 'id.zelory:compressor:3.0.1'
|
implementation 'id.zelory:compressor:3.0.1'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
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.ext:junit:1.2.0-alpha01'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
|
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
|
||||||
|
|||||||
@@ -1,113 +1,78 @@
|
|||||||
package com.vitorpamplona.amethyst
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.ImageUploader
|
||||||
import com.vitorpamplona.amethyst.ui.actions.ImgurServer
|
import com.vitorpamplona.amethyst.ui.actions.ImgurServer
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NostrBuildServer
|
import com.vitorpamplona.amethyst.ui.actions.NostrBuildServer
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NostrFilesDevServer
|
import com.vitorpamplona.amethyst.ui.actions.NostrFilesDevServer
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NostrImgServer
|
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 kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ImageUploadTesting {
|
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=="
|
val image = "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"
|
||||||
|
|
||||||
@Test()
|
fun testBase(server: FileServer) {
|
||||||
fun testImgurUpload() = runBlocking {
|
|
||||||
val bytes = Base64.getDecoder().decode(image)
|
val bytes = Base64.getDecoder().decode(image)
|
||||||
val inputStream = bytes.inputStream()
|
val inputStream = bytes.inputStream()
|
||||||
|
|
||||||
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
var url: String? = null
|
||||||
|
var error: String? = null
|
||||||
|
|
||||||
|
ImageUploader.account = Account(
|
||||||
|
KeyPair()
|
||||||
|
)
|
||||||
|
|
||||||
ImageUploader.uploadImage(
|
ImageUploader.uploadImage(
|
||||||
inputStream,
|
inputStream,
|
||||||
bytes.size.toLong(),
|
bytes.size.toLong(),
|
||||||
"image/gif",
|
"image/gif",
|
||||||
ImgurServer(),
|
server,
|
||||||
onSuccess = { url, contentType ->
|
onSuccess = { newUrl, contentType ->
|
||||||
println("Uploaded to $url")
|
println("Uploaded to $url")
|
||||||
assertNotNull(url)
|
url = newUrl
|
||||||
|
countDownLatch.countDown()
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
println("Failed to Upload")
|
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()
|
@Test()
|
||||||
fun testNostrBuildUpload() = runBlocking {
|
fun testNostrBuildUpload() = runBlocking {
|
||||||
val bytes = Base64.getDecoder().decode(image)
|
testBase(NostrBuildServer())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test()
|
@Test()
|
||||||
fun testNostrImgUpload() = runBlocking {
|
fun testNostrImgUpload() = runBlocking {
|
||||||
val bytes = Base64.getDecoder().decode(image)
|
testBase(NostrImgServer())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test()
|
@Test()
|
||||||
fun testNostrFilesDevUpload() = runBlocking {
|
fun testNostrFilesDevUpload() = runBlocking {
|
||||||
val bytes = Base64.getDecoder().decode(image)
|
testBase(NostrFilesDevServer())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ class Account(
|
|||||||
var hideBlockAlertDialog: Boolean = false,
|
var hideBlockAlertDialog: Boolean = false,
|
||||||
var hideNIP24WarningDialog: Boolean = false,
|
var hideNIP24WarningDialog: Boolean = false,
|
||||||
var backupContactList: ContactListEvent? = null,
|
var backupContactList: ContactListEvent? = null,
|
||||||
var proxy: Proxy?,
|
var proxy: Proxy? = null,
|
||||||
var proxyPort: Int,
|
var proxyPort: Int = 9050,
|
||||||
var showSensitiveContent: Boolean? = null,
|
var showSensitiveContent: Boolean? = null,
|
||||||
var warnAboutPostsWithReports: Boolean = true,
|
var warnAboutPostsWithReports: Boolean = true,
|
||||||
var filterSpamFromStrangers: Boolean = true,
|
var filterSpamFromStrangers: Boolean = true,
|
||||||
|
|||||||
@@ -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 {
|
fun formattedDateTime(timestamp: Long): String {
|
||||||
return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
|
return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
|
||||||
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
|
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
|
||||||
@@ -1497,6 +1517,7 @@ object LocalCache {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
is AdvertisedRelayListEvent -> consume(event)
|
||||||
is AppDefinitionEvent -> consume(event)
|
is AppDefinitionEvent -> consume(event)
|
||||||
is AppRecommendationEvent -> consume(event)
|
is AppRecommendationEvent -> consume(event)
|
||||||
is AudioTrackEvent -> consume(event)
|
is AudioTrackEvent -> consume(event)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ val noProtocolUrlValidator = try {
|
|||||||
Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)")
|
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() {
|
class RichTextParser() {
|
||||||
fun parseText(
|
fun parseText(
|
||||||
@@ -183,7 +183,7 @@ class RichTextParser() {
|
|||||||
} else if (word.contains(".") && schemelessMatcher.find()) {
|
} else if (word.contains(".") && schemelessMatcher.find()) {
|
||||||
val url = schemelessMatcher.group(1) // url
|
val url = schemelessMatcher.group(1) // url
|
||||||
val additionalChars = schemelessMatcher.group(4) // additional chars
|
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) {
|
if (pattern.find(word) != null) {
|
||||||
SchemelessUrlSegment(word, url, additionalChars)
|
SchemelessUrlSegment(word, url, additionalChars)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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 {
|
fun createAccountAcceptedAwardsFilter(): TypedFilter {
|
||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = COMMON_FEED_TYPES,
|
types = COMMON_FEED_TYPES,
|
||||||
@@ -155,6 +166,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
|||||||
accountChannel.typedFilters = listOf(
|
accountChannel.typedFilters = listOf(
|
||||||
createAccountMetadataFilter(),
|
createAccountMetadataFilter(),
|
||||||
createAccountContactListFilter(),
|
createAccountContactListFilter(),
|
||||||
|
createAccountRelayListFilter(),
|
||||||
createNotificationFilter(),
|
createNotificationFilter(),
|
||||||
createGiftWrapsToMeFilter(),
|
createGiftWrapsToMeFilter(),
|
||||||
createAccountReportsFilter(),
|
createAccountReportsFilter(),
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Immutable
|
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.bechToBytes
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Tlv
|
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 com.vitorpamplona.amethyst.service.toNAddress
|
||||||
import fr.acinq.secp256k1.Hex
|
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 toTag() = "$kind:$pubKeyHex:$dTag"
|
||||||
|
|
||||||
fun toNAddr(): String {
|
fun toNAddr(): String {
|
||||||
val kind = kind.toByteArray()
|
return TlvBuilder().apply {
|
||||||
val author = pubKeyHex.hexToByteArray()
|
addString(Tlv.Type.SPECIAL, dTag)
|
||||||
val dTag = dTag.toByteArray(Charsets.UTF_8)
|
addStringIfNotNull(Tlv.Type.RELAY, relay)
|
||||||
val relay = relay?.toByteArray(Charsets.UTF_8)
|
addHex(Tlv.Type.AUTHOR, pubKeyHex)
|
||||||
|
addInt(Tlv.Type.KIND, kind)
|
||||||
var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, dTag.size.toByte()) + dTag
|
}.build().toNAddress()
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -63,10 +51,11 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela
|
|||||||
|
|
||||||
if (key.startsWith("naddr")) {
|
if (key.startsWith("naddr")) {
|
||||||
val tlv = Tlv.parse(key.bechToBytes())
|
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 d = tlv.firstAsString(Tlv.Type.SPECIAL) ?: ""
|
||||||
val author = tlv.get(Tlv.Type.AUTHOR.id)?.get(0)?.toHexKey()
|
val relay = tlv.firstAsString(Tlv.Type.RELAY)
|
||||||
val kind = tlv.get(Tlv.Type.KIND.id)?.get(0)?.let { Tlv.toInt32(it) }
|
val author = tlv.firstAsHex(Tlv.Type.AUTHOR)
|
||||||
|
val kind = tlv.firstAsInt(Tlv.Type.KIND)
|
||||||
|
|
||||||
if (kind != null && author != null) {
|
if (kind != null && author != null) {
|
||||||
return ATag(kind, author, d, relay)
|
return ATag(kind, author, d, relay)
|
||||||
|
|||||||
+70
@@ -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<List<String>>,
|
||||||
|
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<AdvertisedRelayInfo> {
|
||||||
|
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<AdvertisedRelayInfo>,
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ class EventFactory {
|
|||||||
sig: String,
|
sig: String,
|
||||||
lenient: Boolean
|
lenient: Boolean
|
||||||
) = when (kind) {
|
) = when (kind) {
|
||||||
|
AdvertisedRelayListEvent.kind -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
|
AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
|
AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class FileHeaderEvent(
|
|||||||
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
|
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 blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
|
||||||
|
|
||||||
|
fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 1063
|
const val kind = 1063
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ class HighlightEvent(
|
|||||||
msg: String,
|
msg: String,
|
||||||
privateKey: ByteArray,
|
privateKey: ByteArray,
|
||||||
createdAt: Long = TimeUtils.now()
|
createdAt: Long = TimeUtils.now()
|
||||||
): PollNoteEvent {
|
): HighlightEvent {
|
||||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||||
val tags = mutableListOf<List<String>>()
|
val tags = mutableListOf<List<String>>()
|
||||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||||
val sig = CryptoUtils.sign(id, privateKey)
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.service.nip19
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.amethyst.model.hexToByteArray
|
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import com.vitorpamplona.amethyst.service.bechToBytes
|
import com.vitorpamplona.amethyst.service.bechToBytes
|
||||||
import com.vitorpamplona.amethyst.service.toNEvent
|
import com.vitorpamplona.amethyst.service.toNEvent
|
||||||
@@ -82,13 +81,8 @@ object Nip19 {
|
|||||||
private fun nprofile(bytes: ByteArray): Return? {
|
private fun nprofile(bytes: ByteArray): Return? {
|
||||||
val tlv = Tlv.parse(bytes)
|
val tlv = Tlv.parse(bytes)
|
||||||
|
|
||||||
val hex = tlv.get(Tlv.Type.SPECIAL.id)
|
val hex = tlv.firstAsHex(Tlv.Type.SPECIAL) ?: return null
|
||||||
?.get(0)
|
val relay = tlv.firstAsString(Tlv.Type.RELAY)
|
||||||
?.toHexKey() ?: return null
|
|
||||||
|
|
||||||
val relay = tlv.get(Tlv.Type.RELAY.id)
|
|
||||||
?.get(0)
|
|
||||||
?.toString(Charsets.UTF_8)
|
|
||||||
|
|
||||||
return Return(Type.USER, hex, relay)
|
return Return(Type.USER, hex, relay)
|
||||||
}
|
}
|
||||||
@@ -96,30 +90,16 @@ object Nip19 {
|
|||||||
private fun nevent(bytes: ByteArray): Return? {
|
private fun nevent(bytes: ByteArray): Return? {
|
||||||
val tlv = Tlv.parse(bytes)
|
val tlv = Tlv.parse(bytes)
|
||||||
|
|
||||||
val hex = tlv.get(Tlv.Type.SPECIAL.id)
|
val hex = tlv.firstAsHex(Tlv.Type.SPECIAL) ?: return null
|
||||||
?.get(0)
|
val relay = tlv.firstAsString(Tlv.Type.RELAY)
|
||||||
?.toHexKey() ?: return null
|
val author = tlv.firstAsHex(Tlv.Type.AUTHOR)
|
||||||
|
val kind = tlv.firstAsInt(Tlv.Type.KIND.id)
|
||||||
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) }
|
|
||||||
|
|
||||||
return Return(Type.EVENT, hex, relay, author, kind)
|
return Return(Type.EVENT, hex, relay, author, kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun nrelay(bytes: ByteArray): Return? {
|
private fun nrelay(bytes: ByteArray): Return? {
|
||||||
val relayUrl = Tlv.parse(bytes)
|
val relayUrl = Tlv.parse(bytes).firstAsString(Tlv.Type.SPECIAL.id) ?: return null
|
||||||
.get(Tlv.Type.SPECIAL.id)
|
|
||||||
?.get(0)
|
|
||||||
?.toString(Charsets.UTF_8) ?: return null
|
|
||||||
|
|
||||||
return Return(Type.RELAY, relayUrl)
|
return Return(Type.RELAY, relayUrl)
|
||||||
}
|
}
|
||||||
@@ -127,53 +107,20 @@ object Nip19 {
|
|||||||
private fun naddr(bytes: ByteArray): Return? {
|
private fun naddr(bytes: ByteArray): Return? {
|
||||||
val tlv = Tlv.parse(bytes)
|
val tlv = Tlv.parse(bytes)
|
||||||
|
|
||||||
val d = tlv.get(Tlv.Type.SPECIAL.id)
|
val d = tlv.firstAsString(Tlv.Type.SPECIAL.id) ?: ""
|
||||||
?.get(0)
|
val relay = tlv.firstAsString(Tlv.Type.RELAY.id)
|
||||||
?.toString(Charsets.UTF_8) ?: return null
|
val author = tlv.firstAsHex(Tlv.Type.AUTHOR.id) ?: return null
|
||||||
|
val kind = tlv.firstAsInt(Tlv.Type.KIND.id) ?: 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) }
|
|
||||||
|
|
||||||
return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind)
|
return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun createNEvent(idHex: String, author: String?, kind: Int?, relay: String?): String {
|
public fun createNEvent(idHex: String, author: String?, kind: Int?, relay: String?): String {
|
||||||
val kind = kind?.toByteArray()
|
return TlvBuilder().apply {
|
||||||
val author = author?.hexToByteArray()
|
addHex(Tlv.Type.SPECIAL, idHex)
|
||||||
val idHex = idHex.hexToByteArray()
|
addStringIfNotNull(Tlv.Type.RELAY, relay)
|
||||||
val relay = relay?.toByteArray(Charsets.UTF_8)
|
addHexIfNotNull(Tlv.Type.AUTHOR, author)
|
||||||
|
addIntIfNotNull(Tlv.Type.KIND, kind)
|
||||||
var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, idHex.size.toByte()) + idHex
|
}.build().toNEvent()
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Int.toByteArray(): ByteArray {
|
|
||||||
val bytes = ByteArray(4)
|
|
||||||
(0..3).forEach {
|
|
||||||
bytes[3 - it] = ((this ushr (8 * it)) and 0xFFFF).toByte()
|
|
||||||
}
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,67 @@
|
|||||||
package com.vitorpamplona.amethyst.service.nip19
|
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.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
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<Byte, List<ByteArray>>) {
|
||||||
|
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) {
|
enum class Type(val id: Byte) {
|
||||||
SPECIAL(0),
|
SPECIAL(0),
|
||||||
RELAY(1),
|
RELAY(1),
|
||||||
@@ -11,12 +69,9 @@ object Tlv {
|
|||||||
KIND(3);
|
KIND(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toInt32(bytes: ByteArray): Int {
|
companion object {
|
||||||
require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" }
|
|
||||||
return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parse(data: ByteArray): Map<Byte, List<ByteArray>> {
|
fun parse(data: ByteArray): Tlv {
|
||||||
val result = mutableMapOf<Byte, MutableList<ByteArray>>()
|
val result = mutableMapOf<Byte, MutableList<ByteArray>>()
|
||||||
var rest = data
|
var rest = data
|
||||||
while (rest.isNotEmpty()) {
|
while (rest.isNotEmpty()) {
|
||||||
@@ -31,6 +86,7 @@ object Tlv {
|
|||||||
}
|
}
|
||||||
result[t]?.add(v)
|
result[t]?.add(v)
|
||||||
}
|
}
|
||||||
return result
|
return Tlv(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ object ImageUploader {
|
|||||||
|
|
||||||
fun NIP98Header(url: String, method: String, body: String): String {
|
fun NIP98Header(url: String, method: String, body: String): String {
|
||||||
val noteJson = account.createHTTPAuthorization(url, method, body)?.toJson() ?: ""
|
val noteJson = account.createHTTPAuthorization(url, method, body)?.toJson() ?: ""
|
||||||
|
|
||||||
val encodedNIP98Event: String = Base64.getEncoder().encodeToString(noteJson.toByteArray())
|
val encodedNIP98Event: String = Base64.getEncoder().encodeToString(noteJson.toByteArray())
|
||||||
return "Nostr " + encodedNIP98Event
|
return "Nostr " + encodedNIP98Event
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -24,8 +22,9 @@ import androidx.compose.material.Icon
|
|||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.OutlinedTextField
|
import androidx.compose.material.OutlinedTextField
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Cancel
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
import androidx.compose.material.icons.filled.DeleteSweep
|
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.ButtonBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
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.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -76,7 +76,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.lang.Math.round
|
import java.lang.Math.round
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "", nav: (String) -> Unit) {
|
fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "", nav: (String) -> Unit) {
|
||||||
val postViewModel: NewRelayListViewModel = viewModel()
|
val postViewModel: NewRelayListViewModel = viewModel()
|
||||||
@@ -113,26 +112,19 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = { onClose() },
|
onDismissRequest = onClose,
|
||||||
properties = DialogProperties(
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
decorFitsSystemWindows = false,
|
|
||||||
usePlatformDefaultWidth = false,
|
|
||||||
dismissOnClickOutside = false
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Surface(modifier = Modifier.imePadding()) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(10.dp)
|
|
||||||
) {
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().padding(end = 10.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
CloseButton(onCancel = {
|
Spacer(modifier = StdHorzSpacer)
|
||||||
postViewModel.clear()
|
|
||||||
onClose()
|
|
||||||
})
|
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -162,9 +154,30 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re
|
|||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
CloseButton(onCancel = {
|
||||||
|
postViewModel.clear()
|
||||||
|
onClose()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
backgroundColor = MaterialTheme.colors.surface,
|
||||||
|
elevation = 0.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { pad ->
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Spacer(modifier = StdVertSpacer)
|
Column(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
16.dp,
|
||||||
|
pad.calculateTopPadding(),
|
||||||
|
16.dp,
|
||||||
|
pad.calculateBottomPadding()
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.SpaceAround
|
||||||
|
) {
|
||||||
Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ fun SelectTextDialog(text: String, onDismiss: () -> Unit) {
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = onDismiss
|
onClick = onDismiss
|
||||||
) {
|
) {
|
||||||
ArrowBackIcon(Size24dp)
|
ArrowBackIcon()
|
||||||
}
|
}
|
||||||
Text(text = stringResource(R.string.select_text_dialog_top))
|
Text(text = stringResource(R.string.select_text_dialog_top))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextSpinner(
|
fun TextSpinner(
|
||||||
label: String,
|
label: String?,
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
options: ImmutableList<String>,
|
options: ImmutableList<String>,
|
||||||
explainers: ImmutableList<String>? = null,
|
explainers: ImmutableList<String>? = null,
|
||||||
@@ -54,7 +54,7 @@ fun TextSpinner(
|
|||||||
value = currentText,
|
value = currentText,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
label = { Text(label) },
|
label = { label?.let { Text(it) } },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
|||||||
|
|
||||||
return collection
|
return collection
|
||||||
.asSequence()
|
.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 { isGlobal || it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false) || (it.event?.isTaggedGeoHashes(followingGeohashSet) ?: false) }
|
||||||
.filter { isHiddenList || account.isAcceptable(it) }
|
.filter { isHiddenList || account.isAcceptable(it) }
|
||||||
.filter { it.createdAt()!! <= now }
|
.filter { it.createdAt()!! <= now }
|
||||||
|
|||||||
+2
-6
@@ -21,7 +21,6 @@ import androidx.compose.material.Text
|
|||||||
import androidx.compose.material.TextButton
|
import androidx.compose.material.TextButton
|
||||||
import androidx.compose.material.TopAppBar
|
import androidx.compose.material.TopAppBar
|
||||||
import androidx.compose.material.icons.Icons
|
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.Logout
|
||||||
import androidx.compose.material.icons.filled.RadioButtonChecked
|
import androidx.compose.material.icons.filled.RadioButtonChecked
|
||||||
import androidx.compose.runtime.Composable
|
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.actions.toImmutableListOfLists
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
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.note.toShortenHex
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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)) },
|
title = { Text(text = stringResource(R.string.account_switch_add_account_dialog_title)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { popupExpanded = false }) {
|
IconButton(onClick = { popupExpanded = false }) {
|
||||||
Icon(
|
ArrowBackIcon()
|
||||||
imageVector = Icons.Default.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.back),
|
|
||||||
tint = MaterialTheme.colors.onSurface
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backgroundColor = Color.Transparent,
|
backgroundColor = Color.Transparent,
|
||||||
|
|||||||
@@ -8,22 +8,29 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
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.Divider
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.LocalContentAlpha
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.ProvideTextStyle
|
||||||
import androidx.compose.material.ScaffoldState
|
import androidx.compose.material.ScaffoldState
|
||||||
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Text
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -39,20 +46,21 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
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.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.map
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.ChatroomKey
|
||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||||
|
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
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.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
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.LoadAddressableNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
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.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.equalImmutableLists
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DislayGeoTagHeader
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashActionOptions
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagActionOptions
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagHeader
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRoom
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRoom
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRoomByAuthor
|
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.screen.loggedIn.SpinnerSelectionDialog
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
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.HeaderPictureModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
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 com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -108,7 +131,8 @@ fun AppTopBar(
|
|||||||
navEntryState: State<NavBackStackEntry?>,
|
navEntryState: State<NavBackStackEntry?>,
|
||||||
scaffoldState: ScaffoldState,
|
scaffoldState: ScaffoldState,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
nav: (String) -> Unit,
|
||||||
|
navPopBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val currentRoute by remember(navEntryState.value) {
|
val currentRoute by remember(navEntryState.value) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
@@ -122,7 +146,7 @@ fun AppTopBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderTopRouteBar(currentRoute, id, followLists, scaffoldState, accountViewModel, nav)
|
RenderTopRouteBar(currentRoute, id, followLists, scaffoldState, accountViewModel, nav, navPopBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -132,67 +156,24 @@ private fun RenderTopRouteBar(
|
|||||||
followLists: FollowListViewModel,
|
followLists: FollowListViewModel,
|
||||||
scaffoldState: ScaffoldState,
|
scaffoldState: ScaffoldState,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
nav: (String) -> Unit,
|
||||||
|
navPopBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
when (currentRoute) {
|
when (currentRoute) {
|
||||||
// Route.Profile.route -> TopBarWithBackButton(nav)
|
|
||||||
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav)
|
Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||||
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav)
|
Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||||
Route.Discover.base -> DiscoveryTopBar(followLists, scaffoldState, accountViewModel, nav)
|
Route.Discover.base -> DiscoveryTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||||
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel, nav)
|
Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel, nav)
|
||||||
|
Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack)
|
||||||
else -> {
|
else -> {
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
when (currentRoute) {
|
when (currentRoute) {
|
||||||
Route.Channel.base -> LoadChannel(baseChannelHex = id) {
|
Route.Channel.base -> ChannelTopBar(id, accountViewModel, nav, navPopBack)
|
||||||
ChannelHeader(
|
Route.RoomByAuthor.base -> RoomByAuthorTopBar(id, accountViewModel, nav, navPopBack)
|
||||||
baseChannel = it,
|
Route.Room.base -> RoomTopBar(id, accountViewModel, nav, navPopBack)
|
||||||
showVideo = true,
|
Route.Community.base -> CommunityTopBar(id, accountViewModel, nav, navPopBack)
|
||||||
showBottomDiviser = true,
|
Route.Hashtag.base -> HashTagTopBar(id, accountViewModel, navPopBack)
|
||||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 11.dp),
|
Route.Geohash.base -> GeoHashTopBar(id, accountViewModel, navPopBack)
|
||||||
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)
|
|
||||||
else -> MainTopBar(scaffoldState, accountViewModel, nav)
|
else -> MainTopBar(scaffoldState, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
@Composable
|
||||||
fun NoTopBar() {
|
fun NoTopBar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
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)
|
val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||||
|
|
||||||
FollowList(
|
FollowList(
|
||||||
@@ -223,7 +371,7 @@ fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
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)
|
val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS)
|
||||||
|
|
||||||
FollowList(
|
FollowList(
|
||||||
@@ -238,7 +386,7 @@ fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, a
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
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)
|
val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||||
|
|
||||||
FollowList(
|
FollowList(
|
||||||
@@ -253,7 +401,7 @@ fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: Scaffold
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
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)
|
val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||||
|
|
||||||
FollowList(
|
FollowList(
|
||||||
@@ -268,18 +416,23 @@ fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldSta
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel, nav) {
|
GenericMainTopBar(scaffoldState, accountViewModel, nav) {
|
||||||
AmethystIcon()
|
AmethystClickableIcon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(coil.annotation.ExperimentalCoilApi::class)
|
@OptIn(coil.annotation.ExperimentalCoilApi::class)
|
||||||
@Composable
|
@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()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
Column(modifier = BottomTopHeight) {
|
Column(modifier = BottomTopHeight) {
|
||||||
TopAppBar(
|
MyTopAppBar(
|
||||||
elevation = 0.dp,
|
elevation = 0.dp,
|
||||||
backgroundColor = MaterialTheme.colors.surface,
|
backgroundColor = MaterialTheme.colors.surface,
|
||||||
title = {
|
title = {
|
||||||
@@ -291,8 +444,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight()
|
.fillMaxHeight(),
|
||||||
.padding(start = 0.dp, end = 20.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
@@ -488,25 +640,17 @@ fun SimpleTextSpinner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopBarWithBackButton(navController: NavHostController) {
|
fun TopBarWithBackButton(caption: String, popBack: () -> Unit) {
|
||||||
Column() {
|
Column(modifier = BottomTopHeight) {
|
||||||
TopAppBar(
|
MyTopAppBar(
|
||||||
elevation = 0.dp,
|
elevation = 0.dp,
|
||||||
backgroundColor = Color(0xFFFFFF),
|
title = { Text(caption) },
|
||||||
title = {},
|
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = popBack,
|
||||||
navController.popBackStack()
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Icon(
|
ArrowBackIcon()
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
|
||||||
null,
|
|
||||||
modifier = Modifier.size(28.dp),
|
|
||||||
tint = MaterialTheme.colors.primary
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {}
|
actions = {}
|
||||||
@@ -516,7 +660,34 @@ fun TopBarWithBackButton(navController: NavHostController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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
|
val context = LocalContext.current
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -524,12 +695,7 @@ fun AmethystIcon() {
|
|||||||
debugState(context)
|
debugState(context)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
AmethystIcon(Size40dp)
|
||||||
painter = painterResource(R.drawable.amethyst),
|
|
||||||
null,
|
|
||||||
modifier = Modifier.size(40.dp),
|
|
||||||
tint = Color.Unspecified
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,3 +745,151 @@ fun debugState(context: Context) {
|
|||||||
Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value.size} elements ")
|
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)
|
||||||
|
|||||||
@@ -35,6 +35,16 @@ import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.subtleButton
|
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
|
@Composable
|
||||||
fun FollowingIcon(iconSize: Dp) {
|
fun FollowingIcon(iconSize: Dp) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -46,12 +56,11 @@ fun FollowingIcon(iconSize: Dp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArrowBackIcon(iconSize: Dp) {
|
fun ArrowBackIcon() {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ArrowBack,
|
imageVector = Icons.Default.ArrowBack,
|
||||||
contentDescription = null,
|
contentDescription = stringResource(R.string.back),
|
||||||
modifier = remember(iconSize) { Modifier.size(iconSize) },
|
tint = MaterialTheme.colors.onSurface
|
||||||
tint = MaterialTheme.colors.primary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -527,7 +527,11 @@ fun CommunityHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
ShortCommunityHeader(baseNote, expanded, accountViewModel, nav)
|
ShortCommunityHeader(
|
||||||
|
baseNote = baseNote,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
|
|
||||||
if (expanded.value) {
|
if (expanded.value) {
|
||||||
LongCommunityHeader(baseNote, accountViewModel, nav)
|
LongCommunityHeader(baseNote, accountViewModel, nav)
|
||||||
@@ -685,7 +689,7 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState<Boolean>, 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 noteState by baseNote.live().metadata.observeAsState()
|
||||||
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
|
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
|
||||||
|
|
||||||
@@ -710,27 +714,11 @@ fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState<Boole
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = remember(noteState) { noteEvent.dTag() },
|
text = remember(noteState) { noteEvent.dTag() },
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = fontWeight,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val summary = remember(noteState) {
|
|
||||||
noteEvent.description()?.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(
|
Row(
|
||||||
|
|||||||
@@ -75,11 +75,11 @@ private fun UserNameDisplay(
|
|||||||
fontWeight: FontWeight = FontWeight.Bold
|
fontWeight: FontWeight = FontWeight.Bold
|
||||||
) {
|
) {
|
||||||
if (bestUserName != null && bestDisplayName != null && bestDisplayName != bestUserName) {
|
if (bestUserName != null && bestDisplayName != null && bestDisplayName != bestUserName) {
|
||||||
UserAndUsernameDisplay(bestDisplayName, tags, bestUserName, modifier, showPlayButton, fontWeight)
|
UserAndUsernameDisplay(bestDisplayName.trim(), tags, bestUserName.trim(), modifier, showPlayButton, fontWeight)
|
||||||
} else if (bestDisplayName != null) {
|
} else if (bestDisplayName != null) {
|
||||||
UserDisplay(bestDisplayName, tags, modifier, showPlayButton, fontWeight)
|
UserDisplay(bestDisplayName.trim(), tags, modifier, showPlayButton, fontWeight)
|
||||||
} else if (bestUserName != null) {
|
} else if (bestUserName != null) {
|
||||||
UserDisplay(bestUserName, tags, modifier, showPlayButton, fontWeight)
|
UserDisplay(bestUserName.trim(), tags, modifier, showPlayButton, fontWeight)
|
||||||
} else {
|
} else {
|
||||||
NPubDisplay(npubDisplay, modifier, fontWeight)
|
NPubDisplay(npubDisplay, modifier, fontWeight)
|
||||||
}
|
}
|
||||||
@@ -134,16 +134,18 @@ private fun UserAndUsernameDisplay(
|
|||||||
text = bestDisplayName,
|
text = bestDisplayName,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
fontWeight = fontWeight,
|
fontWeight = fontWeight,
|
||||||
maxLines = 1
|
maxLines = 1,
|
||||||
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
/*
|
||||||
CreateTextWithEmoji(
|
CreateTextWithEmoji(
|
||||||
text = remember { "@$bestUserName" },
|
text = remember { "@$bestUserName" },
|
||||||
tags = tags,
|
tags = tags,
|
||||||
color = MaterialTheme.colors.placeholderText,
|
color = MaterialTheme.colors.placeholderText,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = modifier
|
|
||||||
)
|
)*/
|
||||||
if (showPlayButton) {
|
if (showPlayButton) {
|
||||||
Spacer(StdHorzSpacer)
|
Spacer(StdHorzSpacer)
|
||||||
DrawPlayName(bestDisplayName)
|
DrawPlayName(bestDisplayName)
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import androidx.compose.material.icons.filled.Share
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
@@ -577,7 +576,12 @@ fun ChannelHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
ShortChannelHeader(baseChannel, expanded, accountViewModel, nav, showFlag)
|
ShortChannelHeader(
|
||||||
|
baseChannel = baseChannel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
showFlag = showFlag
|
||||||
|
)
|
||||||
|
|
||||||
if (expanded.value) {
|
if (expanded.value) {
|
||||||
LongChannelHeader(baseChannel, accountViewModel, nav)
|
LongChannelHeader(baseChannel, accountViewModel, nav)
|
||||||
@@ -593,7 +597,7 @@ fun ChannelHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ShowVideoStreaming(
|
fun ShowVideoStreaming(
|
||||||
baseChannel: LiveActivitiesChannel,
|
baseChannel: LiveActivitiesChannel,
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
) {
|
) {
|
||||||
@@ -651,10 +655,10 @@ private fun ShowVideoStreaming(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ShortChannelHeader(
|
fun ShortChannelHeader(
|
||||||
baseChannel: Channel,
|
baseChannel: Channel,
|
||||||
expanded: MutableState<Boolean>,
|
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
|
fontWeight: FontWeight = FontWeight.Bold,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
showFlag: Boolean
|
showFlag: Boolean
|
||||||
) {
|
) {
|
||||||
@@ -695,27 +699,11 @@ private fun ShortChannelHeader(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = remember(channelState) { channel.toBestDisplayName() },
|
text = remember(channelState) { channel.toBestDisplayName() },
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = fontWeight,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
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(
|
Row(
|
||||||
@@ -735,7 +723,7 @@ private fun ShortChannelHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LongChannelHeader(
|
fun LongChannelHeader(
|
||||||
baseChannel: Channel,
|
baseChannel: Channel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
nav: (String) -> Unit
|
||||||
@@ -969,6 +957,7 @@ fun LiveFlag() {
|
|||||||
text = stringResource(id = R.string.live_stream_live_tag),
|
text = stringResource(id = R.string.live_stream_live_tag),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
modifier = remember {
|
modifier = remember {
|
||||||
Modifier
|
Modifier
|
||||||
.clip(SmallBorder)
|
.clip(SmallBorder)
|
||||||
|
|||||||
@@ -531,7 +531,7 @@ fun GroupChatroomHeader(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
RoomNameOnlyDisplay(room, Modifier, accountViewModel.userProfile())
|
RoomNameOnlyDisplay(room, Modifier, FontWeight.Bold, accountViewModel.userProfile())
|
||||||
DisplayUserSetAsSubject(room, FontWeight.Normal)
|
DisplayUserSetAsSubject(room, FontWeight.Normal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -714,14 +714,14 @@ fun LongRoomHeader(room: ChatroomKey, accountViewModel: AccountViewModel, nav: (
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 {
|
val roomSubject by loggedInUser.live().messages.map {
|
||||||
it.user.privateChatrooms[room]?.subject
|
it.user.privateChatrooms[room]?.subject
|
||||||
}.distinctUntilChanged().observeAsState(loggedInUser.privateChatrooms[room]?.subject)
|
}.distinctUntilChanged().observeAsState(loggedInUser.privateChatrooms[room]?.subject)
|
||||||
|
|
||||||
Crossfade(targetState = roomSubject, modifier) {
|
Crossfade(targetState = roomSubject, modifier) {
|
||||||
if (it != null && it.isNotBlank()) {
|
if (it != null && it.isNotBlank()) {
|
||||||
DisplayRoomSubject(it)
|
DisplayRoomSubject(it, fontWeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ fun GeoHashHeader(tag: String, modifier: Modifier = StdPadding, account: Account
|
|||||||
) {
|
) {
|
||||||
DislayGeoTagHeader(tag, remember { Modifier.weight(1f) })
|
DislayGeoTagHeader(tag, remember { Modifier.weight(1f) })
|
||||||
|
|
||||||
HashtagActionOptions(tag, account)
|
GeoHashActionOptions(tag, account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ fun DislayGeoTagHeader(geohash: String, modifier: Modifier) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HashtagActionOptions(
|
fun GeoHashActionOptions(
|
||||||
tag: String,
|
tag: String,
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ fun HashtagHeader(tag: String, modifier: Modifier = StdPadding, account: Account
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HashtagActionOptions(
|
fun HashtagActionOptions(
|
||||||
tag: String,
|
tag: String,
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val navPopBack = remember(navController) {
|
||||||
|
{
|
||||||
|
navController.popBackStack()
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val followLists: FollowListViewModel = viewModel(
|
val followLists: FollowListViewModel = viewModel(
|
||||||
key = accountViewModel.userProfile().pubkeyHex + "FollowListViewModel",
|
key = accountViewModel.userProfile().pubkeyHex + "FollowListViewModel",
|
||||||
factory = FollowListViewModel.Factory(accountViewModel.account)
|
factory = FollowListViewModel.Factory(accountViewModel.account)
|
||||||
@@ -203,7 +210,7 @@ fun MainScreen(
|
|||||||
AppBottomBar(accountViewModel, navState, navBottomRow)
|
AppBottomBar(accountViewModel, navState, navBottomRow)
|
||||||
},
|
},
|
||||||
topBar = {
|
topBar = {
|
||||||
AppTopBar(followLists, navState, scaffoldState, accountViewModel, nav = nav)
|
AppTopBar(followLists, navState, scaffoldState, accountViewModel, nav = nav, navPopBack)
|
||||||
},
|
},
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
DrawerContent(nav, scaffoldState, sheetState, accountViewModel)
|
DrawerContent(nav, scaffoldState, sheetState, accountViewModel)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import androidx.compose.material.Scaffold
|
|||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TopAppBar
|
import androidx.compose.material.TopAppBar
|
||||||
import androidx.compose.material.icons.Icons
|
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.Block
|
||||||
import androidx.compose.material.icons.filled.Report
|
import androidx.compose.material.icons.filled.Report
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -41,6 +40,7 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||||
import com.vitorpamplona.amethyst.ui.theme.WarningColor
|
import com.vitorpamplona.amethyst.ui.theme.WarningColor
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
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)) },
|
title = { Text(text = stringResource(id = R.string.report_dialog_title)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onDismiss) {
|
IconButton(onClick = onDismiss) {
|
||||||
Icon(
|
ArrowBackIcon()
|
||||||
imageVector = Icons.Default.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.back),
|
|
||||||
tint = MaterialTheme.colors.onSurface
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backgroundColor = MaterialTheme.colors.surface,
|
backgroundColor = MaterialTheme.colors.surface,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -15,6 +16,7 @@ import androidx.compose.material.DropdownMenuItem
|
|||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.ExposedDropdownMenuBox
|
import androidx.compose.material.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material.ExposedDropdownMenuDefaults
|
import androidx.compose.material.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TextField
|
import androidx.compose.material.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -26,10 +28,12 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.intl.Locale
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.os.LocaleListCompat
|
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.model.parseConnectivityType
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
|
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
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.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
@@ -108,16 +115,18 @@ fun SettingsScreen(
|
|||||||
stringResource(ConnectivityType.WIFI_ONLY.reourceId),
|
stringResource(ConnectivityType.WIFI_ONLY.reourceId),
|
||||||
stringResource(ConnectivityType.NEVER.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(
|
val themeItens = persistentListOf(
|
||||||
stringResource(R.string.system),
|
stringResource(R.string.system),
|
||||||
stringResource(R.string.light),
|
stringResource(R.string.light),
|
||||||
stringResource(R.string.dark)
|
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 themeIndex = themeViewModel.theme.value ?: 0
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -127,21 +136,17 @@ fun SettingsScreen(
|
|||||||
val languageIndex = getLanguageIndex(languageEntries)
|
val languageIndex = getLanguageIndex(languageEntries)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
StdPadding
|
Modifier
|
||||||
|
.padding(top = Size10dp, start = Size20dp, end = Size20dp)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Section(stringResource(R.string.application_preferences))
|
SettingsRow(
|
||||||
|
R.string.language,
|
||||||
Row(
|
R.string.language_description,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
languageList,
|
||||||
modifier = Modifier.fillMaxWidth()
|
languageIndex
|
||||||
) {
|
) {
|
||||||
TextSpinner(
|
|
||||||
label = stringResource(R.string.language),
|
|
||||||
placeholder = languageList[languageIndex],
|
|
||||||
options = languageList,
|
|
||||||
onSelect = {
|
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
val job = scope.launch(Dispatchers.IO) {
|
val job = scope.launch(Dispatchers.IO) {
|
||||||
val locale = languageEntries[languageList[it]]
|
val locale = languageEntries[languageList[it]]
|
||||||
@@ -152,99 +157,112 @@ fun SettingsScreen(
|
|||||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]])
|
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]])
|
||||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Spacer(modifier = HalfVertSpacer)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
SettingsRow(
|
||||||
|
R.string.theme,
|
||||||
|
R.string.theme_description,
|
||||||
|
themeItens,
|
||||||
|
themeIndex
|
||||||
) {
|
) {
|
||||||
TextSpinner(
|
|
||||||
label = stringResource(R.string.theme),
|
|
||||||
placeholder = themeItens[themeIndex],
|
|
||||||
options = themeItens,
|
|
||||||
onSelect = {
|
|
||||||
themeViewModel.onChange(it)
|
themeViewModel.onChange(it)
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
LocalPreferences.updateTheme(it)
|
LocalPreferences.updateTheme(it)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Spacer(modifier = HalfVertSpacer)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
SettingsRow(
|
||||||
|
R.string.automatically_load_images_gifs,
|
||||||
|
R.string.automatically_load_images_gifs_description,
|
||||||
|
selectedItens,
|
||||||
|
showImagesIndex
|
||||||
) {
|
) {
|
||||||
TextSpinner(
|
|
||||||
label = stringResource(R.string.automatically_load_images_gifs),
|
|
||||||
placeholder = selectedItens[index],
|
|
||||||
options = selectedItens,
|
|
||||||
onSelect = {
|
|
||||||
val automaticallyShowImages = parseConnectivityType(it)
|
val automaticallyShowImages = parseConnectivityType(it)
|
||||||
|
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
accountViewModel.updateAutomaticallyShowImages(automaticallyShowImages)
|
accountViewModel.updateAutomaticallyShowImages(automaticallyShowImages)
|
||||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Spacer(modifier = HalfVertSpacer)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
SettingsRow(
|
||||||
|
R.string.automatically_play_videos,
|
||||||
|
R.string.automatically_play_videos_description,
|
||||||
|
selectedItens,
|
||||||
|
videoIndex
|
||||||
) {
|
) {
|
||||||
TextSpinner(
|
|
||||||
label = stringResource(R.string.automatically_play_videos),
|
|
||||||
placeholder = selectedItens[videoIndex],
|
|
||||||
options = selectedItens,
|
|
||||||
onSelect = {
|
|
||||||
val automaticallyStartPlayback = parseConnectivityType(it)
|
val automaticallyStartPlayback = parseConnectivityType(it)
|
||||||
|
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
accountViewModel.updateAutomaticallyStartPlayback(automaticallyStartPlayback)
|
accountViewModel.updateAutomaticallyStartPlayback(automaticallyStartPlayback)
|
||||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Spacer(modifier = HalfVertSpacer)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
SettingsRow(
|
||||||
|
R.string.automatically_show_url_preview,
|
||||||
|
R.string.automatically_show_url_preview_description,
|
||||||
|
selectedItens,
|
||||||
|
linkIndex
|
||||||
) {
|
) {
|
||||||
TextSpinner(
|
|
||||||
label = stringResource(R.string.automatically_show_url_preview),
|
|
||||||
placeholder = selectedItens[linkIndex],
|
|
||||||
options = selectedItens,
|
|
||||||
onSelect = {
|
|
||||||
val automaticallyShowUrlPreview = parseConnectivityType(it)
|
val automaticallyShowUrlPreview = parseConnectivityType(it)
|
||||||
|
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
accountViewModel.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview)
|
accountViewModel.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview)
|
||||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsRow(
|
||||||
|
name: Int,
|
||||||
|
description: Int,
|
||||||
|
selectedItens: ImmutableList<String>,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSpinner(
|
||||||
|
label = "",
|
||||||
|
placeholder = selectedItens[selectedIndex],
|
||||||
|
options = selectedItens,
|
||||||
|
onSelect = onSelect,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ val Size25dp = 25.dp
|
|||||||
val Size30dp = 30.dp
|
val Size30dp = 30.dp
|
||||||
val Size34dp = 34.dp
|
val Size34dp = 34.dp
|
||||||
val Size35dp = 35.dp
|
val Size35dp = 35.dp
|
||||||
|
val Size40dp = 40.dp
|
||||||
val Size55dp = 55.dp
|
val Size55dp = 55.dp
|
||||||
val Size75dp = 75.dp
|
val Size75dp = 75.dp
|
||||||
|
|
||||||
|
|||||||
@@ -500,4 +500,26 @@
|
|||||||
<string name="geohash_explainer">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</string>
|
<string name="geohash_explainer">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</string>
|
||||||
|
|
||||||
<string name="add_sensitive_content_explainer">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í</string>
|
<string name="add_sensitive_content_explainer">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í</string>
|
||||||
|
|
||||||
|
<string name="new_feature_nip24_might_not_be_available_title">Nová funkce</string>
|
||||||
|
<string name="new_feature_nip24_might_not_be_available_description">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.</string>
|
||||||
|
<string name="new_feature_nip24_activate">Aktivovat</string>
|
||||||
|
|
||||||
|
<string name="messages_create_public_chat">Veřejné</string>
|
||||||
|
<string name="messages_new_message">Soukromé</string>
|
||||||
|
<string name="messages_new_message_to">Pro</string>
|
||||||
|
<string name="messages_new_message_subject">Předmět</string>
|
||||||
|
<string name="messages_new_message_subject_caption">Téma konverzace</string>
|
||||||
|
<string name="messages_new_message_to_caption">"@Uživatel1, @Uživatel2, @Uživatel3"</string>
|
||||||
|
|
||||||
|
<string name="messages_group_descriptor">Členové této skupiny</string>
|
||||||
|
<string name="messages_new_subject_message">Vysvětlení členům</string>
|
||||||
|
<string name="messages_new_subject_message_placeholder">Změna názvu pro nové cíle.</string>
|
||||||
|
|
||||||
|
<string name="language_description">Pro rozhraní aplikace</string>
|
||||||
|
<string name="theme_description">Tmavé, světlé nebo systémové téma</string>
|
||||||
|
<string name="automatically_load_images_gifs_description">Automaticky načítat obrázky a GIFy</string>
|
||||||
|
<string name="automatically_play_videos_description">Automaticky přehrávat videa a GIFy</string>
|
||||||
|
<string name="automatically_show_url_preview_description">Zobrazit náhledy URL</string>
|
||||||
|
<string name="load_image_description">Kdy načíst obrázek</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -509,4 +509,26 @@ anz der Bedingungen ist erforderlich</string>
|
|||||||
<string name="geohash_explainer">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</string>
|
<string name="geohash_explainer">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</string>
|
||||||
|
|
||||||
<string name="add_sensitive_content_explainer">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</string>
|
<string name="add_sensitive_content_explainer">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</string>
|
||||||
|
|
||||||
|
<string name="new_feature_nip24_might_not_be_available_title">Neues Feature</string>
|
||||||
|
<string name="new_feature_nip24_might_not_be_available_description">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.</string>
|
||||||
|
<string name="new_feature_nip24_activate">Aktivieren</string>
|
||||||
|
|
||||||
|
<string name="messages_create_public_chat">Öffentlich</string>
|
||||||
|
<string name="messages_new_message">Privat</string>
|
||||||
|
<string name="messages_new_message_to">An</string>
|
||||||
|
<string name="messages_new_message_subject">Betreff</string>
|
||||||
|
<string name="messages_new_message_subject_caption">Gesprächsthema</string>
|
||||||
|
<string name="messages_new_message_to_caption">"@Benutzer1, @Benutzer2, @Benutzer3"</string>
|
||||||
|
|
||||||
|
<string name="messages_group_descriptor">Mitglieder dieser Gruppe</string>
|
||||||
|
<string name="messages_new_subject_message">Erklärung an Mitglieder</string>
|
||||||
|
<string name="messages_new_subject_message_placeholder">Ändern des Namens für die neuen Ziele.</string>
|
||||||
|
|
||||||
|
<string name="language_description">Für die App-Benutzeroberfläche</string>
|
||||||
|
<string name="theme_description">Dunkles, helles oder Systemdesign</string>
|
||||||
|
<string name="automatically_load_images_gifs_description">Bilder und GIFs automatisch laden</string>
|
||||||
|
<string name="automatically_play_videos_description">Videos und GIFs automatisch abspielen</string>
|
||||||
|
<string name="automatically_show_url_preview_description">URL-Vorschauen anzeigen</string>
|
||||||
|
<string name="load_image_description">Wann Bilder geladen werden sollen</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -497,6 +497,28 @@
|
|||||||
<string name="geohash_explainer">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</string>
|
<string name="geohash_explainer">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</string>
|
||||||
|
|
||||||
<string name="add_sensitive_content_explainer">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</string>
|
<string name="add_sensitive_content_explainer">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</string>
|
||||||
|
|
||||||
|
<string name="new_feature_nip24_might_not_be_available_title">Ny Funktion</string>
|
||||||
|
<string name="new_feature_nip24_might_not_be_available_description">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.</string>
|
||||||
|
<string name="new_feature_nip24_activate">Aktivera</string>
|
||||||
|
|
||||||
|
<string name="messages_create_public_chat">Publik</string>
|
||||||
|
<string name="messages_new_message">Privat</string>
|
||||||
|
<string name="messages_new_message_to">Till</string>
|
||||||
|
<string name="messages_new_message_subject">Ämne</string>
|
||||||
|
<string name="messages_new_message_subject_caption">Samtalsämne</string>
|
||||||
|
<string name="messages_new_message_to_caption">"@Användare1, @Användare2, @Användare3"</string>
|
||||||
|
|
||||||
|
<string name="messages_group_descriptor">Medlemmar i denna grupp</string>
|
||||||
|
<string name="messages_new_subject_message">Förklaring till medlemmar</string>
|
||||||
|
<string name="messages_new_subject_message_placeholder">Ändra namnet för de nya målen.</string>
|
||||||
|
|
||||||
|
<string name="language_description">För appens gränssnitt</string>
|
||||||
|
<string name="theme_description">Mörkt, Ljust eller Systemtema</string>
|
||||||
|
<string name="automatically_load_images_gifs_description">Ladda automatiskt bilder och GIFs</string>
|
||||||
|
<string name="automatically_play_videos_description">Spela upp videor och GIFs automatiskt</string>
|
||||||
|
<string name="automatically_show_url_preview_description">Visa förhandsgranskning av URL</string>
|
||||||
|
<string name="load_image_description">När bilder ska laddas</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -493,12 +493,12 @@
|
|||||||
<string name="system">System</string>
|
<string name="system">System</string>
|
||||||
<string name="light">Light</string>
|
<string name="light">Light</string>
|
||||||
<string name="dark">Dark</string>
|
<string name="dark">Dark</string>
|
||||||
<string name="application_preferences">Application preferences</string>
|
<string name="application_preferences">Application Preferences</string>
|
||||||
<string name="language">Language</string>
|
<string name="language">Language</string>
|
||||||
<string name="theme">Theme</string>
|
<string name="theme">Theme</string>
|
||||||
<string name="automatically_load_images_gifs">Automatically load images/gifs</string>
|
<string name="automatically_load_images_gifs">Image Preview</string>
|
||||||
<string name="automatically_play_videos">Automatically play videos</string>
|
<string name="automatically_play_videos">Video Playback</string>
|
||||||
<string name="automatically_show_url_preview">Automatically show url preview</string>
|
<string name="automatically_show_url_preview">URL Preview</string>
|
||||||
<string name="load_image">Load Image</string>
|
<string name="load_image">Load Image</string>
|
||||||
|
|
||||||
<string name="spamming_users">Spammers</string>
|
<string name="spamming_users">Spammers</string>
|
||||||
@@ -536,5 +536,13 @@
|
|||||||
<string name="messages_group_descriptor">Members of this group</string>
|
<string name="messages_group_descriptor">Members of this group</string>
|
||||||
<string name="messages_new_subject_message">Explanation to members</string>
|
<string name="messages_new_subject_message">Explanation to members</string>
|
||||||
<string name="messages_new_subject_message_placeholder">Changing the name for the new goals.</string>
|
<string name="messages_new_subject_message_placeholder">Changing the name for the new goals.</string>
|
||||||
|
|
||||||
<string name="paste_from_clipboard">Paste from clipboard</string>
|
<string name="paste_from_clipboard">Paste from clipboard</string>
|
||||||
|
|
||||||
|
<string name="language_description">For the App\'s Interface</string>
|
||||||
|
<string name="theme_description">Dark, Light or System theme</string>
|
||||||
|
<string name="automatically_load_images_gifs_description">Automatically load images and GIFs</string>
|
||||||
|
<string name="automatically_play_videos_description">Automatically plays videos and GIFs</string>
|
||||||
|
<string name="automatically_show_url_preview_description">Show URL previews</string>
|
||||||
|
<string name="load_image_description">When to load images</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -4,10 +4,13 @@
|
|||||||
<locale android:name="cs"/>
|
<locale android:name="cs"/>
|
||||||
<locale android:name="de"/>
|
<locale android:name="de"/>
|
||||||
<locale android:name="eo"/>
|
<locale android:name="eo"/>
|
||||||
|
<locale android:name="en"/>
|
||||||
|
<locale android:name="en-GB"/>
|
||||||
<locale android:name="es"/>
|
<locale android:name="es"/>
|
||||||
<locale android:name="fa"/>
|
<locale android:name="fa"/>
|
||||||
<locale android:name="fr"/>
|
<locale android:name="fr"/>
|
||||||
<locale android:name="hu"/>
|
<locale android:name="hu"/>
|
||||||
|
<locale android:name="ja"/>
|
||||||
<locale android:name="nl"/>
|
<locale android:name="nl"/>
|
||||||
<locale android:name="pt-BR"/>
|
<locale android:name="pt-BR"/>
|
||||||
<locale android:name="ru"/>
|
<locale android:name="ru"/>
|
||||||
@@ -18,7 +21,4 @@
|
|||||||
<locale android:name="zh"/>
|
<locale android:name="zh"/>
|
||||||
<locale android:name="zh-HK"/>
|
<locale android:name="zh-HK"/>
|
||||||
<locale android:name="zh-TW"/>
|
<locale android:name="zh-TW"/>
|
||||||
<locale android:name="en"/>
|
|
||||||
<locale android:name="en-GB"/>
|
|
||||||
<locale android:name="ja"/>
|
|
||||||
</locale-config>
|
</locale-config>
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
}
|
||||||
@@ -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() }
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user