mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 15:44:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c29bf3f25 | ||
|
|
9642e45775 | ||
|
|
58788d2cd5 | ||
|
|
d7c18341cd | ||
|
|
ee16f6c143 | ||
|
|
d78b55d134 | ||
|
|
ff5e7359e2 | ||
|
|
4fd0f38402 | ||
|
|
5b516f4101 | ||
|
|
1fc390c215 | ||
|
|
2bb09f583b | ||
|
|
b4eba2da02 | ||
|
|
786a173ba5 | ||
|
|
5ef3c8f00e | ||
|
|
d832351bcd | ||
|
|
9ae611831f | ||
|
|
6db287a008 | ||
|
|
99641e1a12 | ||
|
|
abb2f0557d | ||
|
|
76a66e7bd3 | ||
|
|
6cd50a115b | ||
|
|
0ed34cec09 | ||
|
|
c5e3657bfe | ||
|
|
da47965d76 | ||
|
|
a66417ec82 | ||
|
|
62c3c93d2e | ||
|
|
fbecc8b6d1 | ||
|
|
e95afa89ff | ||
|
|
d6f2560c1c | ||
|
|
d320b951ae | ||
|
|
af28e6d466 | ||
|
|
86e6d56dcd | ||
|
|
c66fa1073b | ||
|
|
6d9bffe9e7 | ||
|
|
04158cec6e | ||
|
|
76376a7c40 | ||
|
|
67ea0dcf7f | ||
|
|
cdc8850894 | ||
|
|
ce0c7c842d | ||
|
|
f51f929079 | ||
|
|
d87d2e0ef8 | ||
|
|
a3166f4ff3 | ||
|
|
9340c440f5 | ||
|
|
a7343d5b54 | ||
|
|
0551b82bd9 | ||
|
|
18fca8adac | ||
|
|
5d788ed669 | ||
|
|
54d52e0b77 | ||
|
|
28c92675b2 | ||
|
|
baa9ee0562 | ||
|
|
7326d6be93 | ||
|
|
e187e5392f | ||
|
|
7c8f6eb239 | ||
|
|
6294c65ad6 | ||
|
|
51d9657371 | ||
|
|
32783794ee | ||
|
|
e3858dc830 | ||
|
|
38a9030963 | ||
|
|
29c0c4cf5f | ||
|
|
dcbde81a23 | ||
|
|
56e6f06fe7 | ||
|
|
342b64f0eb | ||
|
|
a64a640165 | ||
|
|
6433830c32 | ||
|
|
3a58819b52 | ||
|
|
5c7be44369 | ||
|
|
6e66d008dc | ||
|
|
c61c4ce16c | ||
|
|
246ce55253 | ||
|
|
a704c81d24 | ||
|
|
9963245ec9 | ||
|
|
64016aa940 | ||
|
|
c6462f102a | ||
|
|
b05083863d | ||
|
|
86fffe6539 | ||
|
|
f94a4cfa65 | ||
|
|
3825642ae7 | ||
|
|
f90f81b2a5 | ||
|
|
bc042e52b0 | ||
|
|
d80b22fa29 | ||
|
|
b63ed4fdbd | ||
|
|
397acce85b | ||
|
|
5305929df3 | ||
|
|
6d4ba6d507 | ||
|
|
a028c2c3fc | ||
|
|
86e5dfa799 |
@@ -13,6 +13,7 @@
|
||||
/.idea/ktfmt.xml
|
||||
/.idea/studiobot.xml
|
||||
/.idea/other.xml
|
||||
/.idea/runConfigurations.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
+4
@@ -37,6 +37,10 @@
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" 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" />
|
||||
|
||||
@@ -16,8 +16,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode 405
|
||||
versionName "0.92.0"
|
||||
versionCode 408
|
||||
versionName "0.92.3"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"98f2acaf9b3eb70d87607942e576f9a086ac28adf37869da9a29f4815cecac86\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -293,6 +293,7 @@ dependencies {
|
||||
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.mockk
|
||||
testImplementation libs.kotlinx.coroutines.test
|
||||
|
||||
androidTestImplementation platform(libs.androidx.compose.bom)
|
||||
androidTestImplementation libs.androidx.junit
|
||||
|
||||
@@ -52,6 +52,7 @@ class ImageUploadTesting {
|
||||
Nip96Retriever()
|
||||
.loadInfo(
|
||||
server.baseUrl,
|
||||
false,
|
||||
)
|
||||
|
||||
val bitmap = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
|
||||
@@ -80,6 +81,7 @@ class ImageUploadTesting {
|
||||
alt = null,
|
||||
sensitiveContent = null,
|
||||
serverInfo,
|
||||
forceProxy = { false },
|
||||
onProgress = {},
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
)
|
||||
@@ -106,7 +108,7 @@ class ImageUploadTesting {
|
||||
Assert.assertTrue("${server.name}: Invalid result url", url.startsWith("http"))
|
||||
|
||||
val imageData: ByteArray =
|
||||
ImageDownloader().waitAndGetImage(url)
|
||||
ImageDownloader().waitAndGetImage(url, false)
|
||||
?: run {
|
||||
fail("${server.name}: Should not be null")
|
||||
return
|
||||
|
||||
@@ -46,7 +46,7 @@ class OkHttpOtsTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(), OkHttpCalendarBuilder())
|
||||
OtsEvent.otsInstance = OpenTimestamps(OkHttpBlockstreamExplorer(forceProxy = { false }), OkHttpCalendarBuilder(forceProxy = { false }))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -57,6 +57,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -74,7 +75,7 @@ data class AccountInfo(
|
||||
val npub: String,
|
||||
val hasPrivKey: Boolean,
|
||||
val loggedInWithExternalSigner: Boolean,
|
||||
val isTransient: Boolean = false,
|
||||
val isTransient: Boolean,
|
||||
)
|
||||
|
||||
private object PrefKeys {
|
||||
@@ -126,7 +127,7 @@ object LocalPreferences {
|
||||
private const val COMMA = ","
|
||||
|
||||
private var currentAccount: String? = null
|
||||
private var savedAccounts: List<AccountInfo>? = null
|
||||
private var savedAccounts: MutableStateFlow<List<AccountInfo>?> = MutableStateFlow(null)
|
||||
private var cachedAccounts: MutableMap<String, AccountSettings?> = mutableMapOf()
|
||||
|
||||
suspend fun currentAccount(): String? {
|
||||
@@ -156,7 +157,7 @@ object LocalPreferences {
|
||||
}
|
||||
|
||||
private suspend fun savedAccounts(): List<AccountInfo> {
|
||||
if (savedAccounts == null) {
|
||||
if (savedAccounts.value == null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
with(encryptedPreferences()) {
|
||||
val newSystemOfAccounts =
|
||||
@@ -165,7 +166,7 @@ object LocalPreferences {
|
||||
}
|
||||
|
||||
if (!newSystemOfAccounts.isNullOrEmpty()) {
|
||||
savedAccounts = newSystemOfAccounts
|
||||
savedAccounts.emit(newSystemOfAccounts)
|
||||
} else {
|
||||
val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(COMMA) ?: listOf()
|
||||
|
||||
@@ -174,27 +175,28 @@ object LocalPreferences {
|
||||
AccountInfo(
|
||||
npub,
|
||||
encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false),
|
||||
(encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "")
|
||||
.isNotBlank(),
|
||||
(encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
savedAccounts = migrated
|
||||
savedAccounts.emit(migrated)
|
||||
|
||||
edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(savedAccounts)) }.apply()
|
||||
edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(savedAccounts.value)) }.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return savedAccounts!!
|
||||
// it's always not null when it gets here.
|
||||
return savedAccounts.value!!
|
||||
}
|
||||
|
||||
fun cachedAccounts() = savedAccounts
|
||||
fun accountsFlow() = savedAccounts
|
||||
|
||||
private suspend fun updateSavedAccounts(accounts: List<AccountInfo>) =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (savedAccounts != accounts) {
|
||||
savedAccounts = accounts
|
||||
savedAccounts.emit(accounts)
|
||||
|
||||
encryptedPreferences()
|
||||
.edit()
|
||||
@@ -269,7 +271,7 @@ object LocalPreferences {
|
||||
*/
|
||||
@SuppressLint("ApplySharedPref")
|
||||
suspend fun updatePrefsForLogout(accountInfo: AccountInfo) {
|
||||
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout")
|
||||
Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}")
|
||||
withContext(Dispatchers.IO) {
|
||||
encryptedPreferences(accountInfo.npub).edit().clear().commit()
|
||||
removeAccount(accountInfo)
|
||||
|
||||
@@ -413,14 +413,15 @@ class Account(
|
||||
combineTransform(
|
||||
connectToRelays,
|
||||
settings.torSettings.torType,
|
||||
settings.torSettings.onionRelaysViaTor,
|
||||
settings.torSettings.trustedRelaysViaTor,
|
||||
) { relays, torType, forceTor ->
|
||||
) { relays, torType, useTorForOnionRelays, useTorForTrustedRelays ->
|
||||
emit(
|
||||
relays
|
||||
.map {
|
||||
RelaySetupInfoToConnect(
|
||||
it.url,
|
||||
torType != TorType.OFF && forceTor,
|
||||
torType != TorType.OFF && checkLocalHostOnionAndThen(it.url, useTorForOnionRelays, useTorForTrustedRelays),
|
||||
it.read,
|
||||
it.write,
|
||||
it.feedTypes,
|
||||
@@ -440,7 +441,12 @@ class Account(
|
||||
).map {
|
||||
RelaySetupInfoToConnect(
|
||||
it.url,
|
||||
settings.torSettings.torType.value != TorType.OFF && settings.torSettings.trustedRelaysViaTor.value,
|
||||
settings.torSettings.torType.value != TorType.OFF &&
|
||||
checkLocalHostOnionAndThen(
|
||||
it.url,
|
||||
settings.torSettings.onionRelaysViaTor.value,
|
||||
settings.torSettings.trustedRelaysViaTor.value,
|
||||
),
|
||||
it.read,
|
||||
it.write,
|
||||
it.feedTypes,
|
||||
@@ -1292,11 +1298,11 @@ class Account(
|
||||
signedEvent = event,
|
||||
relayTemplate =
|
||||
RelaySetupInfoToConnect(
|
||||
nip47.relayUri,
|
||||
shouldUseTorForTrustedRelays(), // this is trusted.
|
||||
true,
|
||||
true,
|
||||
wcListener.feedTypes,
|
||||
url = nip47.relayUri,
|
||||
forceProxy = shouldUseTorForTrustedRelays(), // this is trusted.
|
||||
read = true,
|
||||
write = true,
|
||||
feedTypes = wcListener.feedTypes,
|
||||
),
|
||||
onDone = { wcListener.destroy() },
|
||||
)
|
||||
@@ -3423,11 +3429,17 @@ class Account(
|
||||
private fun checkLocalHostOnionAndThen(
|
||||
normalizedUrl: String,
|
||||
final: Boolean,
|
||||
): Boolean = checkLocalHostOnionAndThen(normalizedUrl, settings.torSettings.onionRelaysViaTor.value, final)
|
||||
|
||||
private fun checkLocalHostOnionAndThen(
|
||||
normalizedUrl: String,
|
||||
isOnionRelaysActive: Boolean,
|
||||
final: Boolean,
|
||||
): Boolean =
|
||||
if (isLocalHost(normalizedUrl)) {
|
||||
false
|
||||
} else if (isOnionUrl(normalizedUrl)) {
|
||||
settings.torSettings.onionRelaysViaTor.value
|
||||
isOnionRelaysActive
|
||||
} else {
|
||||
final
|
||||
}
|
||||
|
||||
@@ -259,10 +259,13 @@ class AccountSettings(
|
||||
// ---
|
||||
// language services
|
||||
// ---
|
||||
fun addDontTranslateFrom(languageCode: String) {
|
||||
fun toggleDontTranslateFrom(languageCode: String) {
|
||||
if (!dontTranslateFrom.contains(languageCode)) {
|
||||
dontTranslateFrom = dontTranslateFrom.plus(languageCode)
|
||||
saveAccountSettings()
|
||||
} else {
|
||||
dontTranslateFrom = dontTranslateFrom.minus(languageCode)
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +287,12 @@ class AccountSettings(
|
||||
if (key !in languagePreferences) {
|
||||
languagePreferences = languagePreferences + Pair(key, preference)
|
||||
saveAccountSettings()
|
||||
} else {
|
||||
if (languagePreferences.get(key) == preference) {
|
||||
languagePreferences = languagePreferences.minus(key)
|
||||
} else {
|
||||
languagePreferences = languagePreferences + Pair(key, preference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,16 +94,31 @@ class VisionPrescription(
|
||||
var dateWritten: String? = null,
|
||||
var prescriber: Reference? = Reference(),
|
||||
var lensSpecification: List<LensSpecification> = arrayListOf(),
|
||||
) : Resource(resourceType, id)
|
||||
) : Resource(resourceType, id) {
|
||||
fun glasses() = lensSpecification.filter { it.product == "lens" }
|
||||
|
||||
fun contacts() = lensSpecification.filter { it.product == "contacts" }
|
||||
|
||||
fun glassesRightEyes() = lensSpecification.filter { it.product == "lens" && it.eye == "right" }
|
||||
|
||||
fun glassesLeftEyes() = lensSpecification.filter { it.product == "lens" && it.eye == "left" }
|
||||
|
||||
fun contactsRightEyes() = lensSpecification.filter { it.product == "contacts" && it.eye == "right" }
|
||||
|
||||
fun contactsLeftEyes() = lensSpecification.filter { it.product == "contacts" && it.eye == "left" }
|
||||
}
|
||||
|
||||
@Stable
|
||||
class LensSpecification(
|
||||
var product: String? = null,
|
||||
var eye: String? = null,
|
||||
var sphere: Double? = null,
|
||||
var cylinder: Double? = null,
|
||||
var axis: Double? = null,
|
||||
var pd: Double? = null,
|
||||
var interAdd: Double? = null,
|
||||
var add: Double? = null,
|
||||
var prism: List<Prism> = arrayListOf(),
|
||||
var prism: Prism? = null,
|
||||
// contact lenses
|
||||
var power: Double? = null,
|
||||
var backCurve: Double? = null,
|
||||
|
||||
@@ -118,10 +118,6 @@ class ZapPaymentHandler(
|
||||
|
||||
onProgress(0.02f)
|
||||
signAllZapRequests(note, pollOption, message, zapType, zapsToSend) { splitZapRequestPairs ->
|
||||
splitZapRequestPairs.forEach {
|
||||
println("AABBCC 1 ${it.key} ${it.value} $amountMilliSats")
|
||||
}
|
||||
|
||||
if (splitZapRequestPairs.isEmpty()) {
|
||||
onProgress(0.00f)
|
||||
return@signAllZapRequests
|
||||
@@ -132,10 +128,6 @@ class ZapPaymentHandler(
|
||||
assembleAllInvoices(splitZapRequestPairs.toList(), amountMilliSats, message, showErrorIfNoLnAddress, forceProxy, onError, onProgress = {
|
||||
onProgress(it * 0.7f + 0.05f) // keeps within range.
|
||||
}, context) {
|
||||
splitZapRequestPairs.forEach {
|
||||
println("AABBCC 2 ${it.key} ${it.value} $amountMilliSats")
|
||||
}
|
||||
|
||||
if (it.isEmpty()) {
|
||||
onProgress(0.00f)
|
||||
return@assembleAllInvoices
|
||||
@@ -225,7 +217,6 @@ class ZapPaymentHandler(
|
||||
) + (authorRelayList ?: emptySet())
|
||||
|
||||
prepareZapRequestIfNeeded(note, pollOption, message, zapType, user, userRelayList) { zapRequestJson ->
|
||||
println("AABBCC middle ${user?.toBestDisplayName()} + $zapRequestJson")
|
||||
onReady(SignAllZapRequestsReturn(zapRequestJson, user))
|
||||
}
|
||||
}
|
||||
@@ -284,20 +275,15 @@ class ZapPaymentHandler(
|
||||
collectSuccessfulSigningOperations<String, Boolean>(
|
||||
operationsInput = invoices,
|
||||
runRequestFor = { invoice: String, onReady ->
|
||||
println("AABBCC sending $invoice")
|
||||
|
||||
account.sendZapPaymentRequestFor(
|
||||
bolt11 = invoice,
|
||||
zappedNote = note,
|
||||
onSent = {
|
||||
println("AABBCC sent $invoice")
|
||||
progressAllPayments += 0.5f / invoices.size
|
||||
onProgress(progressAllPayments)
|
||||
onReady(true)
|
||||
},
|
||||
onResponse = { response ->
|
||||
println("AABBCC response $invoice")
|
||||
|
||||
if (response is PayInvoiceErrorResponse) {
|
||||
progressAllPayments += 0.5f / invoices.size
|
||||
onProgress(progressAllPayments)
|
||||
|
||||
@@ -324,7 +324,7 @@ fun uriToRoute(uri: String?): String? =
|
||||
uri?.let {
|
||||
Nip47WalletConnect.parse(it)
|
||||
val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString())
|
||||
Route.Home.base + "?nip47=" + encodedUri
|
||||
Route.NIP47Setup.base + "?nip47=" + encodedUri
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
+125
-139
@@ -24,7 +24,6 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.net.toUri
|
||||
import com.abedelazizshe.lightcompressorlibrary.CompressionListener
|
||||
import com.abedelazizshe.lightcompressorlibrary.VideoCompressor
|
||||
@@ -33,11 +32,11 @@ import com.abedelazizshe.lightcompressorlibrary.config.AppSpecificStorageConfigu
|
||||
import com.abedelazizshe.lightcompressorlibrary.config.Configuration
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils
|
||||
import id.zelory.compressor.Compressor
|
||||
import id.zelory.compressor.constraint.default
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.UUID
|
||||
|
||||
class MediaCompressor {
|
||||
@@ -49,152 +48,139 @@ class MediaCompressor {
|
||||
onError: (Int) -> Unit,
|
||||
mediaQuality: CompressorQuality,
|
||||
) {
|
||||
if (mediaQuality != CompressorQuality.UNCOMPRESSED) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (contentType?.startsWith("video", true) == true) {
|
||||
val videoQuality =
|
||||
when (mediaQuality) {
|
||||
CompressorQuality.VERY_LOW -> VideoQuality.VERY_LOW
|
||||
CompressorQuality.LOW -> VideoQuality.LOW
|
||||
CompressorQuality.MEDIUM -> VideoQuality.MEDIUM
|
||||
CompressorQuality.HIGH -> VideoQuality.HIGH
|
||||
CompressorQuality.VERY_HIGH -> VideoQuality.VERY_HIGH
|
||||
else -> VideoQuality.MEDIUM // dodgy
|
||||
}
|
||||
Log.d("MediaCompressor", "Using video compression $mediaQuality")
|
||||
VideoCompressor.start(
|
||||
// => This is required
|
||||
context = applicationContext,
|
||||
// => Source can be provided as content uris
|
||||
uris = listOf(uri),
|
||||
isStreamable = false,
|
||||
// THIS STORAGE
|
||||
// sharedStorageConfiguration = SharedStorageConfiguration(
|
||||
// saveAt = SaveLocation.movies, // => default is movies
|
||||
// videoName = "compressed_video" // => required name
|
||||
// ),
|
||||
// OR AND NOT BOTH
|
||||
appSpecificStorageConfiguration = AppSpecificStorageConfiguration(),
|
||||
configureWith =
|
||||
Configuration(
|
||||
quality = videoQuality,
|
||||
// => required name
|
||||
videoNames = listOf(UUID.randomUUID().toString()),
|
||||
),
|
||||
listener =
|
||||
object : CompressionListener {
|
||||
override fun onProgress(
|
||||
index: Int,
|
||||
percent: Float,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onStart(index: Int) {
|
||||
// Compression start
|
||||
}
|
||||
|
||||
override fun onSuccess(
|
||||
index: Int,
|
||||
size: Long,
|
||||
path: String?,
|
||||
) {
|
||||
if (path != null) {
|
||||
Log.d("MediaCompressor", "Video compression success. Compressed size [$size]")
|
||||
onReady(Uri.fromFile(File(path)), contentType, size)
|
||||
} else {
|
||||
Log.d("MediaCompressor", "Video compression successful, but returned null path")
|
||||
onError(R.string.compression_returned_null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
index: Int,
|
||||
failureMessage: String,
|
||||
) {
|
||||
// keeps going with original video
|
||||
Log.d("MediaCompressor", "Video compression failed: $failureMessage")
|
||||
onReady(uri, contentType, null)
|
||||
}
|
||||
|
||||
override fun onCancelled(index: Int) {
|
||||
onError(R.string.compression_cancelled)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else if (
|
||||
contentType?.startsWith("image", true) == true &&
|
||||
!contentType.contains("gif") &&
|
||||
!contentType.contains("svg")
|
||||
) {
|
||||
val imageQuality =
|
||||
when (mediaQuality) {
|
||||
CompressorQuality.VERY_LOW -> 40
|
||||
CompressorQuality.LOW -> 50
|
||||
CompressorQuality.MEDIUM -> 60
|
||||
CompressorQuality.HIGH -> 80
|
||||
CompressorQuality.VERY_HIGH -> 90
|
||||
else -> 60
|
||||
}
|
||||
try {
|
||||
Log.d("MediaCompressor", "Using image compression $mediaQuality")
|
||||
val tempFile = from(uri, contentType, applicationContext)
|
||||
val compressedImageFile =
|
||||
Compressor.compress(applicationContext, tempFile) {
|
||||
default(width = 640, format = Bitmap.CompressFormat.JPEG, quality = imageQuality)
|
||||
}
|
||||
Log.d("MediaCompressor", "Image compression success. Original size [${tempFile.length()}], new size [${compressedImageFile.length()}]")
|
||||
onReady(compressedImageFile.toUri(), contentType, compressedImageFile.length())
|
||||
} catch (e: Exception) {
|
||||
Log.d("MediaCompressor", "Image compression failed: ${e.message}")
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
onReady(uri, contentType, null)
|
||||
}
|
||||
} else {
|
||||
onReady(uri, contentType, null)
|
||||
}
|
||||
} else {
|
||||
Log.d("MediaCompressor", "UNCOMPRESSED quality selected, skip compression and continue with original media.")
|
||||
// Skip compression if user selected uncompressed
|
||||
if (mediaQuality == CompressorQuality.UNCOMPRESSED) {
|
||||
Log.d("MediaCompressor", "UNCOMPRESSED quality selected, skipping compression.")
|
||||
onReady(uri, contentType, null)
|
||||
return
|
||||
}
|
||||
|
||||
checkNotInMainThread()
|
||||
|
||||
// branch into compression based on content type
|
||||
when {
|
||||
contentType?.startsWith("video", ignoreCase = true) == true ->
|
||||
compressVideo(uri, contentType, applicationContext, onReady, onError, mediaQuality)
|
||||
contentType?.startsWith("image", ignoreCase = true) == true &&
|
||||
!contentType.contains("gif") &&
|
||||
!contentType.contains("svg") ->
|
||||
compressImage(uri, contentType, applicationContext, onReady, onError, mediaQuality)
|
||||
else -> onReady(uri, contentType, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun from(
|
||||
uri: Uri?,
|
||||
private fun compressVideo(
|
||||
uri: Uri,
|
||||
contentType: String?,
|
||||
applicationContext: Context,
|
||||
onReady: (Uri, String?, Long?) -> Unit,
|
||||
onError: (Int) -> Unit,
|
||||
mediaQuality: CompressorQuality,
|
||||
) {
|
||||
val videoQuality =
|
||||
when (mediaQuality) {
|
||||
CompressorQuality.VERY_LOW -> VideoQuality.VERY_LOW
|
||||
CompressorQuality.LOW -> VideoQuality.LOW
|
||||
CompressorQuality.MEDIUM -> VideoQuality.MEDIUM
|
||||
CompressorQuality.HIGH -> VideoQuality.HIGH
|
||||
CompressorQuality.VERY_HIGH -> VideoQuality.VERY_HIGH
|
||||
else -> VideoQuality.MEDIUM
|
||||
}
|
||||
|
||||
Log.d("MediaCompressor", "Using video compression $mediaQuality")
|
||||
|
||||
VideoCompressor.start(
|
||||
// => This is required
|
||||
context = applicationContext,
|
||||
// => Source can be provided as content uris
|
||||
uris = listOf(uri),
|
||||
isStreamable = false,
|
||||
// THIS STORAGE
|
||||
// sharedStorageConfiguration = SharedStorageConfiguration(
|
||||
// saveAt = SaveLocation.movies, // => default is movies
|
||||
// videoName = "compressed_video" // => required name
|
||||
// ),
|
||||
// OR AND NOT BOTH
|
||||
appSpecificStorageConfiguration = AppSpecificStorageConfiguration(),
|
||||
configureWith =
|
||||
Configuration(
|
||||
quality = videoQuality,
|
||||
// => required name
|
||||
videoNames = listOf(UUID.randomUUID().toString()),
|
||||
),
|
||||
listener =
|
||||
object : CompressionListener {
|
||||
override fun onProgress(
|
||||
index: Int,
|
||||
percent: Float,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onStart(index: Int) {}
|
||||
|
||||
override fun onSuccess(
|
||||
index: Int,
|
||||
size: Long,
|
||||
path: String?,
|
||||
) {
|
||||
if (path != null) {
|
||||
Log.d("MediaCompressor", "Video compression success. Compressed size [$size]")
|
||||
onReady(Uri.fromFile(File(path)), contentType, size)
|
||||
} else {
|
||||
Log.d("MediaCompressor", "Video compression successful, but returned null path")
|
||||
onError(R.string.compression_returned_null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
index: Int,
|
||||
failureMessage: String,
|
||||
) {
|
||||
Log.d("MediaCompressor", "Video compression failed: $failureMessage")
|
||||
// keeps going with original video
|
||||
onReady(uri, contentType, null)
|
||||
}
|
||||
|
||||
override fun onCancelled(index: Int) {
|
||||
onError(R.string.compression_cancelled)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun compressImage(
|
||||
uri: Uri,
|
||||
contentType: String?,
|
||||
context: Context,
|
||||
): File {
|
||||
val extension =
|
||||
contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||
|
||||
val inputStream = context.contentResolver.openInputStream(uri!!)
|
||||
val fileName: String = UUID.randomUUID().toString() + ".$extension"
|
||||
val splitName: Array<String> = splitFileName(fileName)
|
||||
val tempFile = File.createTempFile(splitName[0], splitName[1])
|
||||
inputStream?.use { input ->
|
||||
FileOutputStream(tempFile).use { output ->
|
||||
val buffer = ByteArray(1024 * 50)
|
||||
var read: Int = input.read(buffer)
|
||||
while (read != -1) {
|
||||
output.write(buffer, 0, read)
|
||||
read = input.read(buffer)
|
||||
}
|
||||
onReady: (Uri, String?, Long?) -> Unit,
|
||||
onError: (Int) -> Unit,
|
||||
mediaQuality: CompressorQuality,
|
||||
) {
|
||||
val imageQuality =
|
||||
when (mediaQuality) {
|
||||
CompressorQuality.VERY_LOW -> 40
|
||||
CompressorQuality.LOW -> 50
|
||||
CompressorQuality.MEDIUM -> 60
|
||||
CompressorQuality.HIGH -> 80
|
||||
CompressorQuality.VERY_HIGH -> 90
|
||||
else -> 60
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
private fun splitFileName(fileName: String): Array<String> {
|
||||
var name = fileName
|
||||
var extension = ""
|
||||
val i = fileName.lastIndexOf(".")
|
||||
if (i != -1) {
|
||||
name = fileName.substring(0, i)
|
||||
extension = fileName.substring(i)
|
||||
try {
|
||||
Log.d("MediaCompressor", "Using image compression $mediaQuality")
|
||||
val tempFile = MediaCompressorFileUtils.from(uri, context)
|
||||
val compressedImageFile =
|
||||
Compressor.compress(context, tempFile) {
|
||||
default(width = 640, format = Bitmap.CompressFormat.JPEG, quality = imageQuality)
|
||||
}
|
||||
Log.d("MediaCompressor", "Image compression success. Original size [${tempFile.length()}], new size [${compressedImageFile.length()}]")
|
||||
onReady(compressedImageFile.toUri(), contentType, compressedImageFile.length())
|
||||
} catch (e: Exception) {
|
||||
Log.d("MediaCompressor", "Image compression failed: ${e.message}")
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
onReady(uri, contentType, null)
|
||||
}
|
||||
return arrayOf(name, extension)
|
||||
}
|
||||
|
||||
fun intToCompressorQuality(mediaQualityFloat: Int): CompressorQuality =
|
||||
|
||||
+3
-2
@@ -126,6 +126,7 @@ fun ZoomableContentView(
|
||||
|
||||
val isLandscapeMode = DeviceUtils.isLandscapeMetric(LocalContext.current)
|
||||
val isFoldableOrLarge = DeviceUtils.windowIsLarge(windowSize = currentWindowSize, isInLandscapeMode = isLandscapeMode)
|
||||
val isOrientationLocked = DeviceUtils.screenOrientationIsLocked(LocalContext.current)
|
||||
|
||||
val contentScale =
|
||||
if (isFiniteHeight) {
|
||||
@@ -160,7 +161,7 @@ fun ZoomableContentView(
|
||||
nostrUriCallback = content.uri,
|
||||
onDialog = {
|
||||
dialogOpen = true
|
||||
// if (!isFoldableOrLarge) {
|
||||
// if (!isFoldableOrLarge && !isOrientationLocked) {
|
||||
// DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity)
|
||||
// }
|
||||
},
|
||||
@@ -200,7 +201,7 @@ fun ZoomableContentView(
|
||||
images,
|
||||
onDismiss = {
|
||||
dialogOpen = false
|
||||
// if (!isFoldableOrLarge) DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity)
|
||||
// if (!isFoldableOrLarge && !isOrientationLocked) DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity)
|
||||
},
|
||||
accountViewModel,
|
||||
)
|
||||
|
||||
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.components.util
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.provider.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.window.core.layout.WindowHeightSizeClass
|
||||
@@ -38,6 +40,31 @@ object DeviceUtils {
|
||||
*/
|
||||
fun isLandscapeMetric(context: Context): Boolean = context.resources.displayMetrics.heightPixels < context.resources.displayMetrics.widthPixels
|
||||
|
||||
/**
|
||||
* Checks if the device's orientation is set to locked.
|
||||
*
|
||||
* Credits: NewPipe devs
|
||||
*/
|
||||
fun screenOrientationIsLocked(context: Context): Boolean {
|
||||
// 1: Screen orientation changes using accelerometer
|
||||
// 0: Screen orientation is locked
|
||||
// if the accelerometer sensor is missing completely, assume locked orientation
|
||||
return (
|
||||
Settings.System.getInt(
|
||||
context.contentResolver,
|
||||
Settings.System.ACCELEROMETER_ROTATION,
|
||||
0,
|
||||
) == 0 ||
|
||||
!context.packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_ACCELEROMETER)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the device's orientation. This works even if the device's orientation
|
||||
* is set to locked.
|
||||
* Thus, to prevent unwanted behaviour,
|
||||
* it's use can be guarded by conditions such as [screenOrientationIsLocked].
|
||||
*/
|
||||
fun changeDeviceOrientation(
|
||||
isInLandscape: Boolean,
|
||||
currentActivity: Activity,
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components.util
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.UUID
|
||||
|
||||
object MediaCompressorFileUtils {
|
||||
fun from(
|
||||
uri: Uri,
|
||||
context: Context,
|
||||
): File {
|
||||
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(context.contentResolver.getType(uri)) ?: ""
|
||||
val suffix = if (extension.isNotEmpty()) ".$extension" else ""
|
||||
val tempFile = File.createTempFile(UUID.randomUUID().toString(), suffix)
|
||||
|
||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
FileOutputStream(tempFile).use { outputStream ->
|
||||
copyStream(inputStream, outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile
|
||||
}
|
||||
|
||||
private fun copyStream(
|
||||
input: InputStream,
|
||||
output: OutputStream,
|
||||
) {
|
||||
input.copyTo(output, bufferSize = 1024 * 50)
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ class VideoFeedFilter(
|
||||
(noteEvent is FileStorageHeaderEvent && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
|
||||
) &&
|
||||
params.match(noteEvent) &&
|
||||
account.isAcceptable(it)
|
||||
(params.isHiddenList || account.isAcceptable(it))
|
||||
}
|
||||
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
|
||||
+17
-3
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.feeds
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
@@ -115,20 +114,35 @@ fun rememberForeverLazyListState(
|
||||
return scrollState
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun rememberForeverPagerState(
|
||||
key: String,
|
||||
initialFirstVisibleItemIndex: Int = 0,
|
||||
initialFirstVisibleItemScrollOffset: Float = 0.0f,
|
||||
pageCount: () -> Int,
|
||||
): PagerState =
|
||||
rememberForeverPagerState(key, initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, pageCount) { initialPage, initialPageOffsetFraction, pageCount ->
|
||||
rememberPagerState(initialPage, initialPageOffsetFraction, pageCount)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberForeverPagerState(
|
||||
key: String,
|
||||
initialFirstVisibleItemIndex: Int = 0,
|
||||
initialFirstVisibleItemScrollOffset: Float = 0.0f,
|
||||
pageCount: () -> Int,
|
||||
rememberPagerStateFunction: @Composable (
|
||||
initialPage: Int,
|
||||
initialPageOffsetFraction: Float,
|
||||
pageCount: () -> Int,
|
||||
) -> PagerState,
|
||||
): PagerState {
|
||||
val savedValue = savedScrollStates[key]
|
||||
val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex
|
||||
val savedOffset = savedValue?.scrollOffsetFraction ?: initialFirstVisibleItemScrollOffset
|
||||
|
||||
val scrollState =
|
||||
rememberPagerState(
|
||||
rememberPagerStateFunction(
|
||||
savedIndex,
|
||||
savedOffset,
|
||||
pageCount,
|
||||
|
||||
+13
-11
@@ -47,7 +47,6 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -55,6 +54,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.forEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -81,14 +82,6 @@ fun AccountSwitchBottomSheet(
|
||||
accountViewModel: AccountViewModel,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
) {
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
val accounts by
|
||||
produceState(initialValue = LocalPreferences.cachedAccounts()) {
|
||||
if (value == null) {
|
||||
value = LocalPreferences.allSavedAccounts()
|
||||
}
|
||||
}
|
||||
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
@@ -103,7 +96,7 @@ fun AccountSwitchBottomSheet(
|
||||
) {
|
||||
Text(stringRes(R.string.account_switch_select_account), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
accounts?.forEach { acc -> DisplayAccount(acc, accountViewModel, accountStateViewModel) }
|
||||
DisplayAllAccounts(accountViewModel, accountStateViewModel)
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -123,13 +116,22 @@ fun AccountSwitchBottomSheet(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayAllAccounts(
|
||||
accountViewModel: AccountViewModel,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
) {
|
||||
val accounts by LocalPreferences.accountsFlow().collectAsStateWithLifecycle()
|
||||
accounts?.forEach { acc -> DisplayAccount(acc, accountViewModel, accountStateViewModel) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayAccount(
|
||||
acc: AccountInfo,
|
||||
accountViewModel: AccountViewModel,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
) {
|
||||
var baseUser by remember {
|
||||
var baseUser by remember(acc) {
|
||||
mutableStateOf<User?>(
|
||||
decodePublicKeyAsHexOrNull(acc.npub)?.let {
|
||||
LocalCache.getUserIfExists(it)
|
||||
|
||||
@@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SecurityFiltersScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen
|
||||
@@ -100,24 +101,7 @@ fun AppNavigation(
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) },
|
||||
) {
|
||||
composable(
|
||||
Route.Home.route,
|
||||
Route.Home.arguments,
|
||||
) {
|
||||
val nip47 = it.arguments?.getString("nip47")
|
||||
|
||||
HomeScreen(accountViewModel, nav, nip47)
|
||||
|
||||
if (nip47 != null) {
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
launch {
|
||||
delay(1000)
|
||||
it.arguments?.remove("nip47")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composable(Route.Home.route) { HomeScreen(accountViewModel, nav) }
|
||||
composable(Route.Message.route) { ChatroomListScreen(accountViewModel, nav) }
|
||||
composable(Route.Video.route) { VideoScreen(accountViewModel, nav) }
|
||||
composable(Route.Discover.route) { DiscoverScreen(accountViewModel, nav) }
|
||||
@@ -282,6 +266,19 @@ fun AppNavigation(
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
Route.NIP47Setup.route,
|
||||
Route.NIP47Setup.arguments,
|
||||
enterTransition = { slideInHorizontallyFromEnd },
|
||||
exitTransition = { scaleOut },
|
||||
popEnterTransition = { scaleIn },
|
||||
popExitTransition = { slideOutHorizontallyToEnd },
|
||||
) {
|
||||
val nip47 = it.arguments?.getString("nip47")
|
||||
|
||||
NIP47SetupScreen(accountViewModel, nav, nip47)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.outlined.CloudUpload
|
||||
import androidx.compose.material.icons.outlined.GroupAdd
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -65,6 +67,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
@@ -478,7 +481,7 @@ fun ListContent(
|
||||
|
||||
IconRow(
|
||||
title = stringRes(R.string.media_servers),
|
||||
icon = androidx.media3.ui.R.drawable.exo_icon_repeat_all,
|
||||
icon = Icons.Outlined.CloudUpload,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
nav.closeDrawer()
|
||||
@@ -494,6 +497,16 @@ fun ListContent(
|
||||
route = Route.BlockedUsers.route,
|
||||
)
|
||||
|
||||
IconRow(
|
||||
title = stringRes(R.string.privacy_options),
|
||||
icon = R.drawable.ic_tor,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
nav.closeDrawer()
|
||||
conectOrbotDialogOpen = true
|
||||
},
|
||||
)
|
||||
|
||||
accountViewModel.account.settings.keyPair.privKey?.let {
|
||||
IconRow(
|
||||
title = stringRes(R.string.backup_keys),
|
||||
@@ -506,18 +519,8 @@ fun ListContent(
|
||||
)
|
||||
}
|
||||
|
||||
IconRow(
|
||||
title = stringRes(R.string.connect_via_tor_short),
|
||||
icon = R.drawable.ic_tor,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = {
|
||||
nav.closeDrawer()
|
||||
conectOrbotDialogOpen = true
|
||||
},
|
||||
)
|
||||
|
||||
NavigationRow(
|
||||
title = stringRes(R.string.settings),
|
||||
title = stringRes(R.string.preferences),
|
||||
icon = Route.Settings.icon,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
nav = nav,
|
||||
@@ -528,7 +531,7 @@ fun ListContent(
|
||||
|
||||
IconRow(
|
||||
title = stringRes(R.string.drawer_accounts),
|
||||
icon = R.drawable.manage_accounts,
|
||||
icon = Icons.Outlined.GroupAdd,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
onClick = openSheet,
|
||||
)
|
||||
@@ -648,6 +651,41 @@ fun IconRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconRow(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
tint: Color,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClickLabel = title,
|
||||
onClick = onClick,
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = IconRowModifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
null,
|
||||
modifier = Size22Modifier,
|
||||
tint = tint,
|
||||
)
|
||||
Text(
|
||||
modifier = IconRowTextModifier,
|
||||
text = title,
|
||||
fontSize = Font18SP,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconRowRelays(
|
||||
accountViewModel: AccountViewModel,
|
||||
|
||||
@@ -52,18 +52,10 @@ sealed class Route(
|
||||
) {
|
||||
object Home :
|
||||
Route(
|
||||
route = "Home?nip47={nip47}",
|
||||
route = "Home",
|
||||
icon = R.drawable.ic_home,
|
||||
notifSize = Modifier.size(Size25dp),
|
||||
iconSize = Modifier.size(Size24dp),
|
||||
arguments =
|
||||
listOf(
|
||||
navArgument("nip47") {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
defaultValue = null
|
||||
},
|
||||
).toImmutableList(),
|
||||
contentDescriptor = R.string.route_home,
|
||||
)
|
||||
|
||||
@@ -92,9 +84,6 @@ sealed class Route(
|
||||
Route(
|
||||
route = "Discover",
|
||||
icon = R.drawable.ic_sensors,
|
||||
// hasNewItems = { accountViewModel, newNotes ->
|
||||
// DiscoverLatestItem.hasNewItems(accountViewModel, newNotes)
|
||||
// },
|
||||
contentDescriptor = R.string.route_discover,
|
||||
)
|
||||
|
||||
@@ -217,6 +206,20 @@ sealed class Route(
|
||||
route = "Settings",
|
||||
icon = R.drawable.ic_settings,
|
||||
)
|
||||
|
||||
object NIP47Setup :
|
||||
Route(
|
||||
route = "NIP47Setup?nip47={nip47}",
|
||||
icon = R.drawable.ic_home,
|
||||
arguments =
|
||||
listOf(
|
||||
navArgument("nip47") {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
defaultValue = null
|
||||
},
|
||||
).toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
fun getRouteWithArguments(navController: NavHostController): String? {
|
||||
|
||||
@@ -24,8 +24,12 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -89,6 +93,7 @@ import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
@@ -107,6 +112,7 @@ import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderReaction
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
@@ -132,6 +138,9 @@ import com.vitorpamplona.amethyst.ui.theme.Size28Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.TinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.defaultTweenDuration
|
||||
import com.vitorpamplona.amethyst.ui.theme.defaultTweenFloatSpec
|
||||
import com.vitorpamplona.amethyst.ui.theme.defaultTweenIntOffsetSpec
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.reactionBox
|
||||
@@ -146,6 +155,7 @@ import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -1010,20 +1020,26 @@ fun ZapReaction(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple24dp,
|
||||
onClick = {
|
||||
zapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } },
|
||||
onMultipleChoices = { wantsToZap = true },
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = showErrorMessageDialog + message
|
||||
}
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
)
|
||||
scope.launch {
|
||||
zapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } },
|
||||
onMultipleChoices = {
|
||||
scope.launch {
|
||||
wantsToZap = true
|
||||
}
|
||||
},
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = showErrorMessageDialog + message
|
||||
}
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
)
|
||||
}
|
||||
},
|
||||
onLongClick = { wantsToChangeZapAmount = true },
|
||||
onDoubleClick = { wantsToSetCustomZap = true },
|
||||
@@ -1039,8 +1055,10 @@ fun ZapReaction(
|
||||
zappingProgress = 0f
|
||||
},
|
||||
onChangeAmount = {
|
||||
wantsToZap = false
|
||||
wantsToChangeZapAmount = true
|
||||
scope.launch {
|
||||
wantsToZap = false
|
||||
wantsToChangeZapAmount = true
|
||||
}
|
||||
},
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
@@ -1245,7 +1263,6 @@ fun ObserveZapAmountText(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun BoostTypeChoicePopup(
|
||||
baseNote: Note,
|
||||
@@ -1255,62 +1272,120 @@ private fun BoostTypeChoicePopup(
|
||||
onQuote: () -> Unit,
|
||||
onRepost: () -> Unit,
|
||||
onFork: () -> Unit,
|
||||
) {
|
||||
val visibilityState = rememberVisibilityState(onDismiss)
|
||||
BoostTypeChoicePopup(
|
||||
baseNote,
|
||||
iconSize,
|
||||
accountViewModel,
|
||||
visibilityState,
|
||||
onQuote,
|
||||
onRepost,
|
||||
onFork,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun BoostTypeChoicePopup(
|
||||
baseNote: Note,
|
||||
iconSize: Dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
visibilityState: MutableTransitionState<Boolean>,
|
||||
onQuote: () -> Unit,
|
||||
onRepost: () -> Unit,
|
||||
onFork: () -> Unit,
|
||||
) {
|
||||
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, iconSizePx),
|
||||
onDismissRequest = { onDismiss() },
|
||||
onDismissRequest = { visibilityState.targetState = false },
|
||||
) {
|
||||
FlowRow {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
accountViewModel.boost(baseNote)
|
||||
onDismiss()
|
||||
} else {
|
||||
onRepost()
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.boost), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visibleState = visibilityState,
|
||||
enter = popupAnimationEnter,
|
||||
exit = popupAnimationExit,
|
||||
) {
|
||||
FlowRow {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
accountViewModel.boost(baseNote)
|
||||
visibilityState.targetState = false
|
||||
} else {
|
||||
onRepost()
|
||||
visibilityState.targetState = false
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.boost), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onQuote,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onQuote,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onFork,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center)
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onFork,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val popupAnimationEnter: EnterTransition =
|
||||
slideInVertically(
|
||||
animationSpec = defaultTweenIntOffsetSpec,
|
||||
initialOffsetY = { it / 2 },
|
||||
) + fadeIn(animationSpec = defaultTweenFloatSpec)
|
||||
|
||||
val popupAnimationExit: ExitTransition =
|
||||
slideOutVertically(
|
||||
animationSpec = defaultTweenIntOffsetSpec,
|
||||
targetOffsetY = { it / 2 },
|
||||
) + fadeOut(animationSpec = defaultTweenFloatSpec)
|
||||
|
||||
@Composable
|
||||
fun rememberVisibilityState(onDismiss: () -> Unit): MutableTransitionState<Boolean> {
|
||||
// Prevent multiple calls to onDismiss()
|
||||
var dismissed by remember { mutableStateOf(false) }
|
||||
|
||||
val visibilityState = remember { MutableTransitionState(false).apply { targetState = true } }
|
||||
LaunchedEffect(visibilityState.targetState) {
|
||||
if (!visibilityState.targetState && !dismissed) {
|
||||
delay(defaultTweenDuration.toLong())
|
||||
dismissed = true
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
return visibilityState
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReactionChoicePopup(
|
||||
baseNote: Note,
|
||||
@@ -1318,6 +1393,18 @@ fun ReactionChoicePopup(
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
) {
|
||||
val visibilityState = rememberVisibilityState(onDismiss)
|
||||
ReactionChoicePopup(baseNote, iconSize, accountViewModel, visibilityState, onChangeAmount)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReactionChoicePopup(
|
||||
baseNote: Note,
|
||||
iconSize: Dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
visibilityState: MutableTransitionState<Boolean>,
|
||||
onChangeAmount: () -> Unit,
|
||||
) {
|
||||
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
|
||||
|
||||
@@ -1328,20 +1415,27 @@ fun ReactionChoicePopup(
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, iconSizePx),
|
||||
onDismissRequest = { onDismiss() },
|
||||
onDismissRequest = { visibilityState.targetState = false },
|
||||
properties = PopupProperties(focusable = true),
|
||||
) {
|
||||
ReactionChoicePopupContent(
|
||||
reactions,
|
||||
toRemove = toRemove,
|
||||
onClick = { reactionType ->
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType,
|
||||
)
|
||||
onDismiss()
|
||||
},
|
||||
onChangeAmount,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visibleState = visibilityState,
|
||||
enter = popupAnimationEnter,
|
||||
exit = popupAnimationExit,
|
||||
) {
|
||||
ReactionChoicePopupContent(
|
||||
reactions,
|
||||
toRemove = toRemove,
|
||||
onClick = { reactionType ->
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType,
|
||||
)
|
||||
visibilityState.targetState = false
|
||||
},
|
||||
onChangeAmount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1485,7 +1579,6 @@ fun ZapAmountChoicePopup(
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ZapAmountChoicePopup(
|
||||
baseNote: Note,
|
||||
@@ -1497,6 +1590,23 @@ fun ZapAmountChoicePopup(
|
||||
onError: (title: String, text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
) {
|
||||
val visibilityState = rememberVisibilityState(onDismiss)
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, visibilityState, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ZapAmountChoicePopup(
|
||||
baseNote: Note,
|
||||
zapAmountChoices: ImmutableList<Long>,
|
||||
accountViewModel: AccountViewModel,
|
||||
popupYOffset: Dp,
|
||||
visibilityState: MutableTransitionState<Boolean>,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (title: String, text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val zapMessage = ""
|
||||
@@ -1506,55 +1616,62 @@ fun ZapAmountChoicePopup(
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, yOffset),
|
||||
onDismissRequest = { onDismiss() },
|
||||
onDismissRequest = { visibilityState.targetState = false },
|
||||
properties = PopupProperties(focusable = true),
|
||||
) {
|
||||
FlowRow(horizontalArrangement = Arrangement.Center) {
|
||||
zapAmountChoices.forEach { amountInSats ->
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
amountInSats * 1000,
|
||||
null,
|
||||
zapMessage,
|
||||
context,
|
||||
true,
|
||||
onError,
|
||||
onProgress,
|
||||
onPayViaIntent,
|
||||
)
|
||||
onDismiss()
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
"⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
onClick = {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
amountInSats * 1000,
|
||||
null,
|
||||
zapMessage,
|
||||
context,
|
||||
true,
|
||||
onError,
|
||||
onProgress,
|
||||
onPayViaIntent,
|
||||
)
|
||||
onDismiss()
|
||||
},
|
||||
onLongClick = { onChangeAmount() },
|
||||
AnimatedVisibility(
|
||||
visibleState = visibilityState,
|
||||
enter = popupAnimationEnter,
|
||||
exit = popupAnimationExit,
|
||||
) {
|
||||
FlowRow(horizontalArrangement = Arrangement.Center) {
|
||||
zapAmountChoices.forEach { amountInSats ->
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
amountInSats * 1000,
|
||||
null,
|
||||
zapMessage,
|
||||
context,
|
||||
true,
|
||||
onError,
|
||||
onProgress,
|
||||
onPayViaIntent,
|
||||
)
|
||||
visibilityState.targetState = false
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
"⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
onClick = {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
amountInSats * 1000,
|
||||
null,
|
||||
zapMessage,
|
||||
context,
|
||||
true,
|
||||
onError,
|
||||
onProgress,
|
||||
onPayViaIntent,
|
||||
)
|
||||
visibilityState.targetState = false
|
||||
},
|
||||
onLongClick = { onChangeAmount() },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+328
-321
@@ -31,7 +31,6 @@ import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
@@ -220,16 +219,66 @@ fun UpdateZapAmountDialog(
|
||||
onClose: () -> Unit,
|
||||
nip47uri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
properties =
|
||||
DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnClickOutside = false,
|
||||
decorFitsSystemWindows = false,
|
||||
),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column {
|
||||
val postViewModel: UpdateZapAmountViewModel =
|
||||
viewModel(
|
||||
key = "UpdateZapAmountViewModel",
|
||||
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings),
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CloseButton(
|
||||
onPress = {
|
||||
postViewModel.cancel()
|
||||
onClose()
|
||||
},
|
||||
)
|
||||
|
||||
SaveButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
},
|
||||
isActive = postViewModel.hasChanged(),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
UpdateZapAmountContent(postViewModel, onClose, nip47uri, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun UpdateZapAmountContent(
|
||||
postViewModel: UpdateZapAmountViewModel,
|
||||
onClose: () -> Unit,
|
||||
nip47uri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
val postViewModel: UpdateZapAmountViewModel =
|
||||
viewModel(
|
||||
key = "UpdateZapAmountViewModel",
|
||||
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings),
|
||||
)
|
||||
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
val zapTypes =
|
||||
@@ -282,337 +331,295 @@ fun UpdateZapAmountDialog(
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
properties =
|
||||
DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnClickOutside = false,
|
||||
decorFitsSystemWindows = false,
|
||||
),
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(10.dp)
|
||||
.fillMaxWidth()
|
||||
.imePadding()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Surface(
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(10.dp).imePadding()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
postViewModel.amountSet.forEach { amountInSats ->
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
onClick = { postViewModel.removeAmount(amountInSats) },
|
||||
) {
|
||||
CloseButton(
|
||||
onPress = {
|
||||
postViewModel.cancel()
|
||||
onClose()
|
||||
},
|
||||
)
|
||||
|
||||
SaveButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost()
|
||||
onClose()
|
||||
},
|
||||
isActive = postViewModel.hasChanged(),
|
||||
Text(
|
||||
"⚡ ${
|
||||
showAmount(
|
||||
amountInSats.toBigDecimal().setScale(1),
|
||||
)
|
||||
} ✖",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.animateContentSize()) {
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
postViewModel.amountSet.forEach { amountInSats ->
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
onClick = { postViewModel.removeAmount(amountInSats) },
|
||||
) {
|
||||
Text(
|
||||
"⚡ ${
|
||||
showAmount(
|
||||
amountInSats.toBigDecimal().setScale(1),
|
||||
)
|
||||
} ✖",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.new_amount_in_sats)) },
|
||||
value = postViewModel.nextAmount,
|
||||
onValueChange = { postViewModel.nextAmount = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "100, 1000, 5000",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier.padding(end = 10.dp).weight(1f),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Button(
|
||||
onClick = { postViewModel.addAmount() },
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(text = stringRes(R.string.add), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.new_amount_in_sats)) },
|
||||
value = postViewModel.nextAmount,
|
||||
onValueChange = { postViewModel.nextAmount = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "100, 1000, 5000",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier.padding(end = 10.dp).weight(1f),
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextSpinner(
|
||||
label = stringRes(id = R.string.zap_type_explainer),
|
||||
placeholder =
|
||||
zapTypes.filter { it.first == accountViewModel.defaultZapType() }.first().second,
|
||||
options = zapOptions,
|
||||
onSelect = { postViewModel.selectedZapType = zapTypes[it].first },
|
||||
modifier = Modifier.weight(1f).padding(end = 5.dp),
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 10.dp),
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
|
||||
var qrScanning by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
stringRes(id = R.string.wallet_connect_service),
|
||||
Modifier.weight(1f),
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
onClose()
|
||||
runCatching { uri.openUri("https://nwc.getalby.com/apps/new?c=Amethyst") }
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.alby),
|
||||
contentDescription = stringRes(id = R.string.accessibility_navigate_to_alby),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
clipboardManager.getText()?.let { postViewModel.copyFromClipboard(it.text) }
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.ContentPaste,
|
||||
contentDescription = stringRes(id = R.string.paste_from_clipboard),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { qrScanning = true }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_qrcode),
|
||||
contentDescription = stringRes(id = R.string.accessibility_scan_qr_code),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
stringRes(id = R.string.wallet_connect_service_explainer),
|
||||
Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
fontSize = Font14SP,
|
||||
)
|
||||
}
|
||||
|
||||
if (qrScanning) {
|
||||
SimpleQrCodeScanner {
|
||||
qrScanning = false
|
||||
if (!it.isNullOrEmpty()) {
|
||||
try {
|
||||
postViewModel.updateNIP47(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
if (e.message != null) {
|
||||
accountViewModel.toast(
|
||||
stringRes(context, R.string.error_parsing_nip47_title),
|
||||
stringRes(context, R.string.error_parsing_nip47, it, e.message!!),
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = { postViewModel.addAmount() },
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(text = stringRes(R.string.add), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextSpinner(
|
||||
label = stringRes(id = R.string.zap_type_explainer),
|
||||
placeholder =
|
||||
zapTypes.filter { it.first == accountViewModel.defaultZapType() }.first().second,
|
||||
options = zapOptions,
|
||||
onSelect = { postViewModel.selectedZapType = zapTypes[it].first },
|
||||
modifier = Modifier.weight(1f).padding(end = 5.dp),
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 10.dp),
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
|
||||
var qrScanning by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
stringRes(id = R.string.wallet_connect_service),
|
||||
Modifier.weight(1f),
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
onClose()
|
||||
runCatching { uri.openUri("https://nwc.getalby.com/apps/new?c=Amethyst") }
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.alby),
|
||||
contentDescription = stringRes(id = R.string.accessibility_navigate_to_alby),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
clipboardManager.getText()?.let { postViewModel.copyFromClipboard(it.text) }
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.ContentPaste,
|
||||
contentDescription = stringRes(id = R.string.paste_from_clipboard),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { qrScanning = true }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_qrcode),
|
||||
contentDescription = stringRes(id = R.string.accessibility_scan_qr_code),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
stringRes(id = R.string.wallet_connect_service_explainer),
|
||||
Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
fontSize = Font14SP,
|
||||
)
|
||||
}
|
||||
|
||||
if (qrScanning) {
|
||||
SimpleQrCodeScanner {
|
||||
qrScanning = false
|
||||
if (!it.isNullOrEmpty()) {
|
||||
try {
|
||||
postViewModel.updateNIP47(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
if (e.message != null) {
|
||||
accountViewModel.toast(
|
||||
stringRes(context, R.string.error_parsing_nip47_title),
|
||||
stringRes(context, R.string.error_parsing_nip47, it, e.message!!),
|
||||
)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
stringRes(context, R.string.error_parsing_nip47_title),
|
||||
stringRes(context, R.string.error_parsing_nip47_no_error, it),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.wallet_connect_service_pubkey)) },
|
||||
value = postViewModel.walletConnectPubkey,
|
||||
onValueChange = { postViewModel.walletConnectPubkey = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "npub, hex",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.wallet_connect_service_relay)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
value = postViewModel.walletConnectRelay,
|
||||
onValueChange = { postViewModel.walletConnectRelay = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "wss://relay.server.com",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val keyguardLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
showPassword = true
|
||||
}
|
||||
}
|
||||
|
||||
val authTitle = stringRes(id = R.string.wallet_connect_service_show_secret)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.wallet_connect_service_secret)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
value = postViewModel.walletConnectSecret,
|
||||
onValueChange = { postViewModel.walletConnectSecret = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.wallet_connect_service_secret_placeholder),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (!showPassword) {
|
||||
authenticate(
|
||||
title = authTitle,
|
||||
context = context,
|
||||
keyguardLauncher = keyguardLauncher,
|
||||
onApproved = { showPassword = true },
|
||||
onError = { title, message -> accountViewModel.toast(title, message) },
|
||||
)
|
||||
} else {
|
||||
showPassword = false
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showPassword) {
|
||||
Icons.Outlined.VisibilityOff
|
||||
} else {
|
||||
Icons.Outlined.Visibility
|
||||
},
|
||||
contentDescription =
|
||||
if (showPassword) {
|
||||
stringRes(R.string.show_password)
|
||||
} else {
|
||||
stringRes(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
stringRes(context, R.string.error_parsing_nip47_title),
|
||||
stringRes(context, R.string.error_parsing_nip47_no_error, it),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.wallet_connect_service_pubkey)) },
|
||||
value = postViewModel.walletConnectPubkey,
|
||||
onValueChange = { postViewModel.walletConnectPubkey = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "npub, hex",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.wallet_connect_service_relay)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
value = postViewModel.walletConnectRelay,
|
||||
onValueChange = { postViewModel.walletConnectRelay = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "wss://relay.server.com",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val keyguardLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
showPassword = true
|
||||
}
|
||||
}
|
||||
|
||||
val authTitle = stringRes(id = R.string.wallet_connect_service_show_secret)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.wallet_connect_service_secret)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
value = postViewModel.walletConnectSecret,
|
||||
onValueChange = { postViewModel.walletConnectSecret = it },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.wallet_connect_service_secret_placeholder),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (!showPassword) {
|
||||
authenticate(
|
||||
title = authTitle,
|
||||
context = context,
|
||||
keyguardLauncher = keyguardLauncher,
|
||||
onApproved = { showPassword = true },
|
||||
onError = { title, message -> accountViewModel.toast(title, message) },
|
||||
)
|
||||
} else {
|
||||
showPassword = false
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showPassword) {
|
||||
Icons.Outlined.VisibilityOff
|
||||
} else {
|
||||
Icons.Outlined.Visibility
|
||||
},
|
||||
contentDescription =
|
||||
if (showPassword) {
|
||||
stringRes(R.string.show_password)
|
||||
} else {
|
||||
stringRes(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ import com.vitorpamplona.amethyst.model.LensSpecification
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.Patient
|
||||
import com.vitorpamplona.amethyst.model.Practitioner
|
||||
import com.vitorpamplona.amethyst.model.Prism
|
||||
import com.vitorpamplona.amethyst.model.Reference
|
||||
import com.vitorpamplona.amethyst.model.Resource
|
||||
import com.vitorpamplona.amethyst.model.VisionPrescription
|
||||
import com.vitorpamplona.amethyst.model.findReferenceInDb
|
||||
@@ -58,10 +60,12 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.FhirResourceEvent
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import kotlin.math.abs
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@@ -74,6 +78,97 @@ fun RenderEyeGlassesPrescriptionPreview() {
|
||||
RenderFhirResource(prescriptionEvent)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderEyeGlassesPrescription2Preview() {
|
||||
val vision =
|
||||
VisionPrescription(
|
||||
id = "1",
|
||||
status = null,
|
||||
created = null,
|
||||
patient = Reference(),
|
||||
encounter = Reference(),
|
||||
dateWritten = null,
|
||||
prescriber = Reference(),
|
||||
lensSpecification =
|
||||
listOf(
|
||||
LensSpecification(
|
||||
product = "lens",
|
||||
eye = "right",
|
||||
sphere = -1.00,
|
||||
cylinder = -2.00,
|
||||
axis = 180.0,
|
||||
pd = 31.0,
|
||||
interAdd = 1.50,
|
||||
add = 1.75,
|
||||
prism = Prism(12.0, "down"),
|
||||
power = null,
|
||||
diameter = null,
|
||||
color = null,
|
||||
brand = null,
|
||||
note = null,
|
||||
),
|
||||
LensSpecification(
|
||||
product = "lens",
|
||||
eye = "left",
|
||||
sphere = -1.00,
|
||||
cylinder = -2.00,
|
||||
axis = 180.0,
|
||||
pd = 31.0,
|
||||
interAdd = 1.50,
|
||||
add = 1.75,
|
||||
prism = Prism(12.0, "down"),
|
||||
power = null,
|
||||
diameter = null,
|
||||
color = null,
|
||||
brand = null,
|
||||
note = null,
|
||||
),
|
||||
LensSpecification(
|
||||
product = "contacts",
|
||||
eye = "right",
|
||||
sphere = -1.00,
|
||||
cylinder = -2.00,
|
||||
axis = 180.0,
|
||||
pd = null,
|
||||
interAdd = null,
|
||||
add = null,
|
||||
prism = null,
|
||||
backCurve = 12.0,
|
||||
power = 1.2,
|
||||
diameter = 1.2,
|
||||
color = "blue",
|
||||
brand = "Blue Glasses",
|
||||
note = "note",
|
||||
),
|
||||
LensSpecification(
|
||||
product = "contacts",
|
||||
eye = "left",
|
||||
sphere = -1.00,
|
||||
cylinder = -2.00,
|
||||
axis = 180.0,
|
||||
pd = null,
|
||||
interAdd = null,
|
||||
add = null,
|
||||
prism = null,
|
||||
backCurve = 12.0,
|
||||
power = 1.2,
|
||||
diameter = 1.2,
|
||||
color = "blue",
|
||||
brand = "Blue Glasses",
|
||||
note = "note",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val db =
|
||||
mapOf(
|
||||
"1" to vision,
|
||||
).toImmutableMap()
|
||||
|
||||
RenderEyeGlassesPrescription(vision, db)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderFhirResource(
|
||||
baseNote: Note,
|
||||
@@ -99,7 +194,7 @@ fun RenderFhirResource(event: FhirResourceEvent) {
|
||||
state.baseResource?.let { resource ->
|
||||
when (resource) {
|
||||
is Bundle -> {
|
||||
val vision = resource.entry.filterIsInstance(VisionPrescription::class.java)
|
||||
val vision = resource.entry.filterIsInstance<VisionPrescription>()
|
||||
|
||||
vision.firstOrNull()?.let {
|
||||
RenderEyeGlassesPrescription(it, state.localDb)
|
||||
@@ -125,11 +220,25 @@ fun RenderEyeGlassesPrescription(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = Size10dp),
|
||||
) {
|
||||
val rightEye = visionPrescription.lensSpecification.firstOrNull { it.eye == "right" }
|
||||
val leftEye = visionPrescription.lensSpecification.firstOrNull { it.eye == "left" }
|
||||
val glassesRightEye = visionPrescription.glassesRightEyes().firstOrNull()
|
||||
val glassesLeftEye = visionPrescription.glassesLeftEyes().firstOrNull()
|
||||
|
||||
val contactsRightEye = visionPrescription.contactsRightEyes().firstOrNull()
|
||||
val contactsLeftEye = visionPrescription.contactsLeftEyes().firstOrNull()
|
||||
|
||||
val isGlasses = glassesRightEye != null || glassesLeftEye != null
|
||||
val isContacts = contactsRightEye != null || contactsLeftEye != null
|
||||
|
||||
Text(
|
||||
"Eyeglasses Prescription",
|
||||
if (isGlasses && isContacts) {
|
||||
"Vision Prescription"
|
||||
} else if (isGlasses) {
|
||||
"Glasses Prescription"
|
||||
} else if (isContacts) {
|
||||
"Contact Lenses Prescription"
|
||||
} else {
|
||||
"Empty Prescription"
|
||||
},
|
||||
modifier = Modifier.padding(4.dp).fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
@@ -155,17 +264,36 @@ fun RenderEyeGlassesPrescription(
|
||||
|
||||
Spacer(DoubleVertSpacer)
|
||||
|
||||
RenderEyeGlassesPrescriptionHeaderRow()
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
|
||||
rightEye?.let {
|
||||
RenderEyeGlassesPrescriptionRow(data = it)
|
||||
if (isGlasses) {
|
||||
RenderEyeGlassesPrescriptionHeaderRow()
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
|
||||
glassesRightEye?.let {
|
||||
RenderEyeGlassesPrescriptionRow(data = it)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
|
||||
glassesLeftEye?.let {
|
||||
RenderEyeGlassesPrescriptionRow(data = it)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
|
||||
leftEye?.let {
|
||||
RenderEyeGlassesPrescriptionRow(data = it)
|
||||
Spacer(DoubleVertSpacer)
|
||||
|
||||
if (isContacts) {
|
||||
RenderEyeContactsPrescriptionHeaderRow()
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
|
||||
contactsRightEye?.let {
|
||||
RenderEyeContactsPrescriptionRow(data = it)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
|
||||
contactsLeftEye?.let {
|
||||
RenderEyeContactsPrescriptionRow(data = it)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
|
||||
visionPrescription.prescriber?.reference?.let {
|
||||
@@ -211,7 +339,7 @@ fun RenderEyeGlassesPrescriptionHeaderRow() {
|
||||
)
|
||||
VerticalDivider(thickness = DividerThickness)
|
||||
Text(
|
||||
text = "Add",
|
||||
text = "PD",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
@@ -220,14 +348,15 @@ fun RenderEyeGlassesPrescriptionHeaderRow() {
|
||||
|
||||
@Composable
|
||||
fun RenderEyeGlassesPrescriptionRow(data: LensSpecification) {
|
||||
val numberFormat = DecimalFormat("##.00")
|
||||
val pdFormat = DecimalFormat("##.0")
|
||||
val integerFormat = DecimalFormat("###")
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val numberFormat = DecimalFormat("##.00")
|
||||
val integerFormat = DecimalFormat("###")
|
||||
|
||||
Text(
|
||||
text = data.eye?.capitalize() ?: "Unknown",
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
@@ -250,11 +379,241 @@ fun RenderEyeGlassesPrescriptionRow(data: LensSpecification) {
|
||||
)
|
||||
VerticalDivider(thickness = DividerThickness)
|
||||
Text(
|
||||
text = formatOrBlank(data.add, numberFormat),
|
||||
text = formatOrBlank(data.pd, pdFormat),
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
}
|
||||
|
||||
if (data.interAdd != null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "",
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
VerticalDivider(thickness = DividerThickness)
|
||||
|
||||
if (data.interAdd != null) {
|
||||
Text(
|
||||
text = "Inter Add:",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(2f),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = formatOrBlank(data.interAdd, numberFormat),
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(2f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.add != null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "",
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
VerticalDivider(thickness = DividerThickness)
|
||||
|
||||
if (data.add != null) {
|
||||
Text(
|
||||
text = "Add:",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(2f),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = formatOrBlank(data.add, numberFormat),
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(2f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.prism?.amount != null || data.prism?.base != null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "",
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
VerticalDivider(thickness = DividerThickness)
|
||||
|
||||
Text(
|
||||
text = "Prism:",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(2f),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = formatOrBlank(data.prism?.amount, numberFormat),
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = data.prism?.base ?: "",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderEyeContactsPrescriptionHeaderRow() {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = "Eye",
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
VerticalDivider(thickness = DividerThickness)
|
||||
Text(
|
||||
text = "Sph",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = "Cyl",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = "Axis",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderEyeContactsPrescriptionRow(data: LensSpecification) {
|
||||
val numberFormat = DecimalFormat("##.00")
|
||||
val integerFormat = DecimalFormat("###")
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = data.eye?.capitalize() ?: "Unknown",
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
VerticalDivider(thickness = DividerThickness)
|
||||
Text(
|
||||
text = formatOrBlank(data.sphere, numberFormat),
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = formatOrBlank(data.cylinder, numberFormat),
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = formatOrBlank(data.axis, integerFormat),
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
}
|
||||
|
||||
if (data.backCurve != null || data.diameter != null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
VerticalDivider(thickness = DividerThickness)
|
||||
Text(
|
||||
text = "",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = "Curve:",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = formatOrBlank(data.backCurve, numberFormat),
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.backCurve != null || data.diameter != null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
VerticalDivider(thickness = DividerThickness)
|
||||
Text(
|
||||
text = "",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = "Diameter:",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = formatOrBlank(data.diameter, numberFormat),
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.brand != null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(1f),
|
||||
)
|
||||
VerticalDivider(thickness = DividerThickness)
|
||||
Text(
|
||||
text = data.brand ?: "",
|
||||
textAlign = TextAlign.Right,
|
||||
modifier = Modifier.padding(4.dp).weight(3f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatOrBlank(
|
||||
@@ -262,6 +621,6 @@ fun formatOrBlank(
|
||||
numberFormat: NumberFormat,
|
||||
): String {
|
||||
if (amount == null) return ""
|
||||
if (Math.abs(amount) < 0.01) return ""
|
||||
if (abs(amount) < 0.01) return ""
|
||||
return numberFormat.format(amount)
|
||||
}
|
||||
|
||||
+29
-4
@@ -27,6 +27,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.LocalPreferences.currentAccount
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.DefaultChannels
|
||||
import com.vitorpamplona.amethyst.model.DefaultDMRelayList
|
||||
@@ -109,7 +110,12 @@ class AccountStateViewModel : ViewModel() {
|
||||
is Nip19Bech32.NEmbed -> null
|
||||
is Nip19Bech32.NRelay -> null
|
||||
is Nip19Bech32.NAddress -> null
|
||||
else -> null
|
||||
else ->
|
||||
try {
|
||||
if (loginWithExternalSigner) Hex.decode(key) else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (loginWithExternalSigner && pubKeyParsed == null) {
|
||||
@@ -316,11 +322,30 @@ class AccountStateViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun currentAccount() =
|
||||
when (val state = _accountContent.value) {
|
||||
is AccountState.LoggedIn ->
|
||||
state.accountSettings.keyPair.pubKey
|
||||
.toNpub()
|
||||
|
||||
is AccountState.LoggedInViewOnly ->
|
||||
state.accountSettings.keyPair.pubKey
|
||||
.toNpub()
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun logOff(accountInfo: AccountInfo) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
prepareLogoutOrSwitch()
|
||||
LocalPreferences.updatePrefsForLogout(accountInfo)
|
||||
tryLoginExistingAccount()
|
||||
if (accountInfo.npub == currentAccount()) {
|
||||
// log off and relogin with the 0 account
|
||||
prepareLogoutOrSwitch()
|
||||
LocalPreferences.updatePrefsForLogout(accountInfo)
|
||||
tryLoginExistingAccount()
|
||||
} else {
|
||||
// delete without login off
|
||||
LocalPreferences.updatePrefsForLogout(accountInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1267,7 +1267,7 @@ class AccountViewModel(
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
Log.d(
|
||||
"Rendering Metrics",
|
||||
"Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()}",
|
||||
"Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()} with ${newNotes.size} new notes",
|
||||
)
|
||||
feedStates.updateFeedsWith(newNotes)
|
||||
upgradeAttestations()
|
||||
|
||||
+58
-38
@@ -20,6 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -38,6 +44,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
@@ -76,46 +83,56 @@ fun ChannelFabColumn(
|
||||
}
|
||||
|
||||
Column {
|
||||
if (isOpen) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
wantsToSendNewMessage = true
|
||||
isOpen = false
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.messages_new_message),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = Font12SP,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isOpen,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||
) {
|
||||
Column {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
wantsToSendNewMessage = true
|
||||
isOpen = false
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.messages_new_message),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = Font12SP,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
wantsToCreateChannel = true
|
||||
isOpen = false
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.messages_create_public_chat),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = Font12SP,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
wantsToCreateChannel = true
|
||||
isOpen = false
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.messages_create_public_chat),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = Font12SP,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
val rotationDegree by animateFloatAsState(
|
||||
targetValue = if (isOpen) 45f else 0f,
|
||||
)
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = { isOpen = !isOpen },
|
||||
modifier = Size55Modifier,
|
||||
@@ -125,7 +142,10 @@ fun ChannelFabColumn(
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription = stringRes(R.string.messages_create_public_private_chat_description),
|
||||
modifier = Modifier.size(26.dp),
|
||||
modifier =
|
||||
Modifier.size(26.dp).graphicsLayer {
|
||||
rotationZ = rotationDegree
|
||||
},
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
|
||||
-17
@@ -63,7 +63,6 @@ import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -78,10 +77,7 @@ import kotlinx.coroutines.launch
|
||||
fun HomeScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
nip47: String? = null,
|
||||
) {
|
||||
ResolveNIP47(nip47, accountViewModel)
|
||||
|
||||
HomeScreen(
|
||||
newThreadsFeedState = accountViewModel.feedStates.homeNewThreads,
|
||||
repliesFeedState = accountViewModel.feedStates.homeReplies,
|
||||
@@ -139,18 +135,6 @@ private fun AssembleHomeTabs(
|
||||
inner(pagerState, tabs)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResolveNIP47(
|
||||
nip47: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
var wantsToAddNip47 by remember(nip47) { mutableStateOf(nip47) }
|
||||
|
||||
if (wantsToAddNip47 != null) {
|
||||
UpdateZapAmountDialog({ wantsToAddNip47 = null }, wantsToAddNip47, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchLifeCycleChanges(accountViewModel: AccountViewModel) {
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
@@ -169,7 +153,6 @@ private fun WatchLifeCycleChanges(accountViewModel: AccountViewModel) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun HomePages(
|
||||
pagerState: PagerState,
|
||||
tabs: ImmutableList<TabItem>,
|
||||
|
||||
+10
-1
@@ -20,6 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -73,7 +78,11 @@ fun SummaryBar(state: NotificationSummaryState) {
|
||||
|
||||
UserReactionsRow(state) { showChart = !showChart }
|
||||
|
||||
if (showChart) {
|
||||
AnimatedVisibility(
|
||||
visible = showChart,
|
||||
enter = slideInVertically() + expandVertically(),
|
||||
exit = slideOutVertically() + shrinkVertically(),
|
||||
) {
|
||||
val lineChartCount =
|
||||
lineChart(
|
||||
lines =
|
||||
|
||||
+9
-2
@@ -26,6 +26,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
@@ -43,6 +44,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
class SearchBarViewModel(
|
||||
val account: Account,
|
||||
) : ViewModel() {
|
||||
val focusRequester = FocusRequester()
|
||||
var searchValue by mutableStateOf("")
|
||||
|
||||
private var _searchResultsUsers = MutableStateFlow<List<User>>(emptyList())
|
||||
@@ -74,8 +76,13 @@ class SearchBarViewModel(
|
||||
_searchResultsUsers.emit(
|
||||
LocalCache
|
||||
.findUsersStartingWith(searchValue)
|
||||
.sortedWith(compareBy({ account.isFollowing(it) }, { it.toBestDisplayName() }))
|
||||
.reversed(),
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ it.toBestDisplayName().startsWith(searchValue, true) },
|
||||
{ account.isFollowing(it) },
|
||||
{ it.toBestDisplayName() },
|
||||
),
|
||||
).reversed(),
|
||||
)
|
||||
_searchResultsNotes.emit(
|
||||
LocalCache
|
||||
|
||||
+5
-12
@@ -52,7 +52,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
@@ -91,7 +90,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -148,6 +146,10 @@ fun SearchScreen(
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(searchBarViewModel.focusRequester) {
|
||||
searchBarViewModel.focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
@@ -234,15 +236,6 @@ private fun SearchTextField(
|
||||
modifier: Modifier,
|
||||
onTextChanges: (String) -> Unit,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
delay(100)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier.padding(10.dp).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -264,7 +257,7 @@ private fun SearchTextField(
|
||||
Modifier
|
||||
.weight(1f, true)
|
||||
.defaultMinSize(minHeight = 20.dp)
|
||||
.focusRequester(focusRequester),
|
||||
.focusRequester(searchBarViewModel.focusRequester),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.npub_hex_username),
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.rememberHeightDecreaser
|
||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountContent
|
||||
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NIP47SetupScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
nip47: String?,
|
||||
) {
|
||||
val postViewModel: UpdateZapAmountViewModel =
|
||||
viewModel(
|
||||
key = "UpdateZapAmountViewModel",
|
||||
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings),
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
scrollBehavior = rememberHeightDecreaser(),
|
||||
title = { Text(stringRes(id = R.string.wallet_connect)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
postViewModel.cancel()
|
||||
nav.popBack()
|
||||
},
|
||||
modifier = Modifier,
|
||||
) {
|
||||
ArrowBackIcon()
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
SaveButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost()
|
||||
nav.popBack()
|
||||
},
|
||||
isActive = postViewModel.hasChanged(),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column(Modifier.padding(it)) {
|
||||
UpdateZapAmountContent(postViewModel, onClose = {
|
||||
postViewModel.cancel()
|
||||
nav.popBack()
|
||||
}, nip47, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-15
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -196,9 +197,9 @@ import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import com.vitorpamplona.quartz.events.VideoEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -235,26 +236,32 @@ fun RenderThreadFeed(
|
||||
nav: INav,
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
val firstTimeScrolled = remember { TimeUtils.now() }
|
||||
|
||||
LaunchedEffect(noteId) {
|
||||
// waits to load the thread to scroll to item.
|
||||
delay(100)
|
||||
val noteForPosition =
|
||||
items.list
|
||||
.filter { it.idHex == noteId }
|
||||
.firstOrNull()
|
||||
var position = items.list.indexOf(noteForPosition)
|
||||
|
||||
if (position >= 0) {
|
||||
if (position >= 1 && position < items.list.size - 1) {
|
||||
position-- // show the replying note
|
||||
LaunchedEffect(noteId, items.list) {
|
||||
// hack to allow multiple scrolls to Item while posts on the screen load.
|
||||
// This is important when clicking on a reply of an older thread in Notifications
|
||||
// In that case, this screen will open with 0-1 items, and the scrollToItem below
|
||||
// will not change the state of the screen (too few items, scroll is not available)
|
||||
// as the app loads the reaming of the thread the position of the reply changes
|
||||
// and becuase there wasn't a possibility to scroll before and now there is one,
|
||||
// the screen stays at the top. Once the thread has enough replies, the lazy column
|
||||
// updates with new items correctly. It just needs a few items to start the scrool.
|
||||
//
|
||||
// This hack allows the list 1 second to fill up with more
|
||||
// records before setting up the position on the feed.
|
||||
//
|
||||
// It jumps around, but it is the best we can do.
|
||||
if (TimeUtils.now() - firstTimeScrolled < 1000) {
|
||||
val position = items.list.indexOfFirst { it.idHex == noteId }
|
||||
if (position >= 0) {
|
||||
listState.scrollToItem(position, -200)
|
||||
}
|
||||
|
||||
listState.scrollToItem(position)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
) {
|
||||
|
||||
+75
-43
@@ -25,7 +25,12 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -36,6 +41,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddPhotoAlternate
|
||||
import androidx.compose.material.icons.filled.CameraAlt
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -186,44 +192,51 @@ fun NewImageButton(
|
||||
ShowProgress(postViewModel)
|
||||
} else {
|
||||
Column {
|
||||
if (isOpen) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
wantsToPostFromCamera = true
|
||||
isOpen = false
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CameraAlt,
|
||||
contentDescription = stringRes(id = R.string.upload_image),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
// if (isOpen) {
|
||||
AnimatedVisibility(
|
||||
visible = isOpen,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||
) {
|
||||
Column {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
wantsToPostFromCamera = true
|
||||
isOpen = false
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CameraAlt,
|
||||
contentDescription = stringRes(id = R.string.upload_image),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
wantsToPostFromGallery = true
|
||||
isOpen = false
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AddPhotoAlternate,
|
||||
contentDescription = stringRes(id = R.string.upload_image),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
wantsToPostFromGallery = true
|
||||
isOpen = false
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AddPhotoAlternate,
|
||||
contentDescription = stringRes(id = R.string.upload_image),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
@@ -234,12 +247,31 @@ fun NewImageButton(
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_compose),
|
||||
contentDescription = stringRes(id = R.string.new_short),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isOpen,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringRes(id = R.string.new_short),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !isOpen,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_compose),
|
||||
contentDescription = stringRes(id = R.string.new_short),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.video
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -47,11 +46,11 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
@@ -236,7 +235,6 @@ private fun LoadedState(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SlidingCarousel(
|
||||
loaded: FeedState.Loaded,
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.theme
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -40,6 +41,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Shapes =
|
||||
@@ -287,3 +289,7 @@ val reactionBox =
|
||||
.padding(5.dp)
|
||||
|
||||
val ripple24dp = ripple(bounded = false, radius = Size24dp)
|
||||
|
||||
val defaultTweenDuration = 100
|
||||
val defaultTweenFloatSpec = tween<Float>(durationMillis = defaultTweenDuration)
|
||||
val defaultTweenIntOffsetSpec = tween<IntOffset>(durationMillis = defaultTweenDuration)
|
||||
|
||||
@@ -358,6 +358,7 @@
|
||||
<string name="delete_media_server">Odstranit server médií</string>
|
||||
<string name="upload_server_relays_nip95">Vaše relé (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Soubory jsou hostovány vašimi relé. Nový NIP: zkontrolujte, zda je podporují</string>
|
||||
<string name="privacy_options">Nastavení ochrany soukromí</string>
|
||||
<string name="connect_via_tor_short">Nastavení Tor/Orbot</string>
|
||||
<string name="connect_via_tor">Připojit se přes váš Orbot</string>
|
||||
<string name="connect_via_tor1">Upravit</string>
|
||||
@@ -386,6 +387,7 @@
|
||||
<string name="tor_preset_explainer">Rychle upravte všechna nastavení níže</string>
|
||||
<string name="tor_use_onion_address">Onion URL/Relays</string>
|
||||
<string name="tor_use_onion_address_explainer">Používat Tor pro jakoukoli .onion adresu</string>
|
||||
<string name="tor_use_dm_relays">Relé pro DM</string>
|
||||
<string name="tor_use_dm_relays_explainer">Vynutit Tor pro odesílání a přijímání DMs</string>
|
||||
<string name="tor_use_new_relays">Nedůvěryhodné relé</string>
|
||||
<string name="tor_use_new_relays_explainer">Vynutit Tor na odchozí/příchozí relé</string>
|
||||
@@ -406,6 +408,7 @@
|
||||
<string name="tor_use_nip96_uploads">Nahrávání médií</string>
|
||||
<string name="tor_use_nip96_uploads_explainer">Vynutit Tor při nahrávání obsahu</string>
|
||||
<string name="tor_internal">Interní</string>
|
||||
<string name="tor_external">Orbot</string>
|
||||
<string name="tor_off">Vypnuto</string>
|
||||
<string name="tor_when_needed">Základní</string>
|
||||
<string name="tor_default">Výchozí</string>
|
||||
@@ -502,6 +505,7 @@
|
||||
<string name="community_no_descriptor">Tato komunita nemá popis. Promluvte si s majitelem, aby ho přidal/a.</string>
|
||||
<string name="add_sensitive_content_label">Citlivý obsah</string>
|
||||
<string name="add_sensitive_content_description">Před zobrazením tohoto obsahu přidá upozornění na citlivý obsah.</string>
|
||||
<string name="preferences">Nastavení aplikace</string>
|
||||
<string name="settings">Nastavení</string>
|
||||
<string name="connectivity_type_always">Vždy</string>
|
||||
<string name="connectivity_type_wifi_only">Pouze Wi-Fi</string>
|
||||
@@ -514,6 +518,7 @@
|
||||
<string name="light">Světlý</string>
|
||||
<string name="dark">Tmavý</string>
|
||||
<string name="application_preferences">Předvolby aplikace</string>
|
||||
<string name="wallet_connect">Připojení peněženky</string>
|
||||
<string name="language">Jazyk</string>
|
||||
<string name="theme">Motiv</string>
|
||||
<string name="automatically_load_images_gifs">Automaticky načítat obrázky/gif</string>
|
||||
|
||||
@@ -364,6 +364,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="delete_media_server">Medienserver löschen</string>
|
||||
<string name="upload_server_relays_nip95">Deine Relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Dateien werden von deinen Relays gehostet. Neuer NIP: Überprüfe, ob sie unterstützt werden</string>
|
||||
<string name="privacy_options">Privatsphäre-Optionen</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot-Einrichtung</string>
|
||||
<string name="connect_via_tor">Verbindung über deine Orbot-Einrichtung herstellen</string>
|
||||
<string name="connect_via_tor1">Anpassen</string>
|
||||
@@ -403,6 +404,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="tor_use_url_previews_explainer">Erzwingen Sie Tor beim Laden von URL-Vorschauen</string>
|
||||
<string name="tor_use_images">Bilder</string>
|
||||
<string name="tor_use_images_explainer">Erzwingen Sie Tor beim Laden von Bildern</string>
|
||||
<string name="tor_use_videos">Video</string>
|
||||
<string name="tor_use_videos_explainer">Erzwingen Sie Tor beim Laden von Videos</string>
|
||||
<string name="tor_use_money_operations">Geldtransaktionen</string>
|
||||
<string name="tor_use_money_operations_explainer">Erzwingen Sie Tor bei Zaps, Lightning und Cashu-Transfers</string>
|
||||
@@ -411,6 +413,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="tor_use_nip96_uploads">Medien-Uploads</string>
|
||||
<string name="tor_use_nip96_uploads_explainer">Erzwingen Sie Tor beim Hochladen von Inhalten</string>
|
||||
<string name="tor_internal">Intern</string>
|
||||
<string name="tor_external">Orbot</string>
|
||||
<string name="tor_off">Aus</string>
|
||||
<string name="tor_when_needed">Grundlegend</string>
|
||||
<string name="tor_default">Standard</string>
|
||||
@@ -507,6 +510,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="community_no_descriptor">Diese Community hat keine Beschreibung. Sprechen Sie mit dem Eigentümer, um eine hinzuzufügen.</string>
|
||||
<string name="add_sensitive_content_label">Sensibler Inhalt</string>
|
||||
<string name="add_sensitive_content_description">Fügt eine Warnung für sensiblen Inhalt hinzu, bevor dieser Inhalt angezeigt wird.</string>
|
||||
<string name="preferences">App Einstellungen</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="connectivity_type_always">Immer</string>
|
||||
<string name="connectivity_type_wifi_only">Nur WLAN</string>
|
||||
@@ -519,6 +523,7 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="light">Hell</string>
|
||||
<string name="dark">Dunkel</string>
|
||||
<string name="application_preferences">Anwendungseinstellungen</string>
|
||||
<string name="wallet_connect">Wallet Verbindung</string>
|
||||
<string name="language">Sprache</string>
|
||||
<string name="theme">Design</string>
|
||||
<string name="automatically_load_images_gifs">Bilder/GIFs automatisch laden</string>
|
||||
|
||||
@@ -360,6 +360,7 @@
|
||||
<string name="delete_media_server">Supprimer le serveur média</string>
|
||||
<string name="upload_server_relays_nip95">Vos relais (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Les fichiers sont hébergés par vos relais. Nouveau NIP: vérifiez s\'ils sont supportés</string>
|
||||
<string name="privacy_options">Options de confidentialité</string>
|
||||
<string name="connect_via_tor_short">Configuration Tor/Orbot</string>
|
||||
<string name="connect_via_tor">Se connecter à travers la configuration Orbot</string>
|
||||
<string name="connect_via_tor1">Ajuster</string>
|
||||
@@ -506,6 +507,7 @@
|
||||
<string name="community_no_descriptor">Cette communauté n\'a pas de description. Parlez au propriétaire pour en ajouter une</string>
|
||||
<string name="add_sensitive_content_label">Contenu sensible</string>
|
||||
<string name="add_sensitive_content_description">Ajouter un avertissement de contenu sensible avant de montrer ce contenu</string>
|
||||
<string name="preferences">Préférences de l\'application</string>
|
||||
<string name="settings">Paramètres</string>
|
||||
<string name="connectivity_type_always">Toujours</string>
|
||||
<string name="connectivity_type_wifi_only">Wifi uniquement</string>
|
||||
@@ -518,6 +520,7 @@
|
||||
<string name="light">Clair</string>
|
||||
<string name="dark">Sombre</string>
|
||||
<string name="application_preferences">Préférences de l\'application</string>
|
||||
<string name="wallet_connect">Wallet Connect</string>
|
||||
<string name="language">Langage</string>
|
||||
<string name="theme">Thème</string>
|
||||
<string name="automatically_load_images_gifs">Prévisualisation des images</string>
|
||||
@@ -765,7 +768,7 @@
|
||||
<string name="dm_relays_regular">Utilisation des relais réguliers</string>
|
||||
<string name="dm_relays_not_found">Configurer vos relais de messagerie privée</string>
|
||||
<string name="dm_relays_not_found_description">Ce paramètre informe tout le monde les relais à utiliser pour vous envoyer des messages. Sans eux, vous risquez de manquer certains messages.</string>
|
||||
<string name="dm_relays_not_found_examples">Les bonnes options sont:\n - inbox.nostr.wine (payant)\n - you.nostr1.com (relais personnels - payant)</string>
|
||||
<string name="dm_relays_not_found_examples">Les bonnes options sont:\n - inbox.nostr.wine (payant)\n - auth.nostr1.com (gratuit)\n - you.nostr1.com (relais personnels - payant)</string>
|
||||
<string name="dm_relays_not_found_editing">Insérez entre 1-3 relais pour utiliser de boîte de réception privée. Les relais de boîte de réception MP doivent accepter les messages de tout le monde, mais ne vous permet que de les télécharger.</string>
|
||||
<string name="dm_relays_not_found_create_now">Configurer maintenant</string>
|
||||
<string name="search_relays_title">Relais de recherche</string>
|
||||
@@ -779,7 +782,7 @@
|
||||
<string name="public_notif_section">Relais de messagerie publique</string>
|
||||
<string name="public_notif_section_explainer">Ce type de relais reçoit toutes les réponses, commentaires, likes et zaps de vos messages. Insérez de 1 à 3 relais et assurez-vous qu\'ils acceptent les messages de n\'importe qui.</string>
|
||||
<string name="private_inbox_section">Relais de la boîte de réception MP</string>
|
||||
<string name="private_inbox_section_explainer">Insérez de 1 à 3 relais pour servir de boîte de réception privée. Les autres utilisateurs utiliseront ces relais pour vous envoyer des MPs. Les relais de la boîte de réception MP doivent accepter tout message de tout le monde, mais ne vous permet que de les télécharger. De bonnes options sont:\n - inbox.nostr.wine (payant)\n - you.nostr1.com (relais personnels - payant)</string>
|
||||
<string name="private_inbox_section_explainer">Insérez de 1 à 3 relais pour servir de boîte de réception privée. Les autres utilisateurs utiliseront ces relais pour vous envoyer des MPs. Les relais de la boîte de réception MP doivent accepter tout message de tout le monde, mais ne vous permet que de les télécharger. De bonnes options sont:\n - inbox.nostr.wine (payant)\n - auth.nostr1.com (gratuit)\n - you.nostr1.com (relais personnels - payant)</string>
|
||||
<string name="private_outbox_section">Relais privés</string>
|
||||
<string name="private_outbox_section_explainer">Insérez entre 1 et 3 relais pour stocker des événements que personne d\'autre ne peut voir, comme vos Brouillons et/ou paramètres d\'application. Idéalement, ces relais sont soit locaux soit nécessitent une authentification avant de télécharger le contenu de chaque utilisateur.</string>
|
||||
<string name="kind_3_section">Relais généraux</string>
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
<string name="save_to_gallery">चित्रालय में अभिलेखन करें</string>
|
||||
<string name="image_saved_to_the_gallery">चित्र का अभिलेखन किया गया चित्रालय क्रमक में</string>
|
||||
<string name="failed_to_save_the_image">चित्र का अभिलेखन असफल</string>
|
||||
<string name="video_saved_to_the_gallery">चलचित्र को संचारयन्त्र के चलचित्रालय में सुरक्षित रखा गया</string>
|
||||
<string name="failed_to_save_the_video">चलचित्र को सुरक्षित रखने में असफल</string>
|
||||
<string name="upload_image">चित्र आरोहण</string>
|
||||
<string name="uploading">आरोहण चल रहा है…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">उपयोगकर्ता का कोई लैटनिंग पता स्थापित नहीं जिसपर साट्स प्राप्त कर सके</string>
|
||||
@@ -358,8 +360,11 @@
|
||||
<string name="delete_media_server">प्रसारसंगणक मिटाएँ</string>
|
||||
<string name="upload_server_relays_nip95">आपके पुनःप्रसारक (निप॰-९५)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">अभिलेख आपके पुनःप्रसारक द्वारा रखे जाते हैं। नया निप॰: जाँच करें यदि वे अवलम्बन करते हैं</string>
|
||||
<string name="privacy_options">गोपनीयता विकल्प</string>
|
||||
<string name="connect_via_tor_short">टोर / ओर्बोट स्थापन</string>
|
||||
<string name="connect_via_tor">आपकी ओर्बोट स्थापना द्वारा संयोजन करें</string>
|
||||
<string name="connect_via_tor1">अनुकूल करें</string>
|
||||
<string name="connect_via_tor2">टोर स्थापना विकल्प</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">क्या आपके ओर्बोट / टोर से संयोजन काट दें</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">आपके तथ्य तत्क्षणतः सामान्य जाल से चलने लगेंगे</string>
|
||||
<string name="yes">हाँ</string>
|
||||
@@ -368,6 +373,7 @@
|
||||
<string name="follow_list_kind3follows">सभी अनुचरित</string>
|
||||
<string name="follow_list_global">वैश्विक</string>
|
||||
<string name="follow_list_mute_list">मौन सूची</string>
|
||||
<string name="connect_through_your_orbot_setup_short">मूलविकल्प द्वार ९०५० है</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">## टोर द्वारा संयोजन करें ओर्बोट के साथ
|
||||
\n\n१. स्थापित करें [ओर्बोट](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n२. ओर्बोट चलाएँ
|
||||
@@ -377,6 +383,45 @@
|
||||
\n६. सक्रिय करें सूचकयुक्त घुण्डी दबाएँ ओर्बोट को प्रतिनिधि के रूप में प्रयोग करने के लिए
|
||||
</string>
|
||||
<string name="orbot_socks_port">ओर्बोट सोक्स॰ द्वार</string>
|
||||
<string name="use_internal_tor">सक्रिय टोर उपकरण</string>
|
||||
<string name="use_internal_tor_explainer">आन्तरीय संस्करण का प्रयोग करें अथवा ओर्बोट</string>
|
||||
<string name="tor_preset">टोर/गोपनीयता पूर्वकृत स्थापना विकल्प</string>
|
||||
<string name="tor_preset_explainer">शीघ्र से नीचे के सभी स्थापना विकल्पों का परिवर्तन करें</string>
|
||||
<string name="tor_use_onion_address">बहुरहस्यावृत संचार जालपता/पुनःप्रसारक</string>
|
||||
<string name="tor_use_onion_address_explainer">टोर का प्रयोग करें किसी भी .onion जालपता के लिए</string>
|
||||
<string name="tor_use_dm_relays">सीधा संदेश पुनःप्रसारक</string>
|
||||
<string name="tor_use_dm_relays_explainer">सीधे संदेश भेजने तथा प्राप्त करने के लिए टोर का प्रयोग अवश्य करें</string>
|
||||
<string name="tor_use_new_relays">अविश्वसनीय पुनःप्रसारक</string>
|
||||
<string name="tor_use_new_relays_explainer">निर्गत/आगतपेटिका पुनःप्रसारकों के लिए टोर का प्रयोग अवश्य करें</string>
|
||||
<string name="tor_use_trusted_relays">विश्वसनीय पुनःप्रसारक</string>
|
||||
<string name="tor_use_trusted_relays_explainer">आपके सूचियों में सभी पुनःप्रसारकों के लिए टोर का प्रयोग अवश्य करें</string>
|
||||
<string name="tor_use_profile_pictures">परिचय चित्र</string>
|
||||
<string name="tor_use_profile_pictures_explainer">परिचय चित्र अवरोहण के लिए टोर का प्रयोग अवश्य करें</string>
|
||||
<string name="tor_use_url_previews">जालपता पूर्वीक्षण</string>
|
||||
<string name="tor_use_url_previews_explainer">जालपता पूर्वीक्षण के लिए टोर का प्रयोग अवश्य करें</string>
|
||||
<string name="tor_use_images">चित्र</string>
|
||||
<string name="tor_use_images_explainer">चित्र अवरोहण के लिए टोर का प्रयोग अवश्य करें</string>
|
||||
<string name="tor_use_videos">चलचित्र</string>
|
||||
<string name="tor_use_videos_explainer">चलचित्र अवरोहण के लिए टोर का प्रयोग अवश्य करें</string>
|
||||
<string name="tor_use_money_operations">व्यापार</string>
|
||||
<string name="tor_use_money_operations_explainer">ज्साप लैटनिंग तथा काशयु व्यापार के लिए टोर का प्रयोग अवश्य करें</string>
|
||||
<string name="tor_use_nip05_verification">नोस्ट्र पता सत्यापन</string>
|
||||
<string name="tor_use_nip05_verification_explainer">निप॰-०५ पता सत्यापन के लिए टोर का प्रयोग अवश्य करें</string>
|
||||
<string name="tor_use_nip96_uploads">चित्र चलचित्र अभिलेख आरोहण</string>
|
||||
<string name="tor_use_nip96_uploads_explainer">चित्र चलचित्र अभिलेख आरोहण के लिए टोर का प्रयोग अवश्य करें</string>
|
||||
<string name="tor_internal">आन्तरीय</string>
|
||||
<string name="tor_external">ओर्बोट</string>
|
||||
<string name="tor_off">निष्क्रिय</string>
|
||||
<string name="tor_when_needed">मूलभूत</string>
|
||||
<string name="tor_default">मूलविकल्प</string>
|
||||
<string name="tor_small_payloads">चित्र चलचित्र अभिलेख के अतिरिक्त सभी</string>
|
||||
<string name="tor_full_privacy">सम्पूर्णतः गुप्त</string>
|
||||
<string name="tor_custom">विशिष्ट</string>
|
||||
<string name="tor_when_needed_explainer">सेवासंगणक की आवश्यकता होने पर टोर का प्रयोग करें</string>
|
||||
<string name="tor_default_explainer">अपना अंकीय जालपता छिपाएँ अपरिचित पुनःप्रसारकों से</string>
|
||||
<string name="tor_small_payloads_explainer">चित्र चलचित्र अतिरिक्त अन्य सभी से अपना अंकीय जालपता छिपाएँ</string>
|
||||
<string name="tor_full_privacy_explainer">सभी संयोजनों में अपना अंकीय जालपता छिपाएँ</string>
|
||||
<string name="tor_custom_explainer">अपना स्वयम का बनाएँ</string>
|
||||
<string name="invalid_port_number">द्वार अंक अमान्य</string>
|
||||
<string name="use_orbot">ओर्बोट का प्रयोग करें</string>
|
||||
<string name="disconnect_from_your_orbot_setup">टोर / ओर्बोट संयोजन काट दें</string>
|
||||
@@ -462,6 +507,7 @@
|
||||
<string name="community_no_descriptor">इस समुदाय का कोई विवरण नहीं। इसके अधिपति से बात करें इसे जोडने के लिए</string>
|
||||
<string name="add_sensitive_content_label">संवेदनशील विषयवस्तु</string>
|
||||
<string name="add_sensitive_content_description">इसे दिखाने से पूर्व संवेदनशील विषयवस्तु चेतावनी जोडता है</string>
|
||||
<string name="preferences">क्रमक आद्यताएँ</string>
|
||||
<string name="settings">स्थापना विकल्प</string>
|
||||
<string name="connectivity_type_always">सर्वदा</string>
|
||||
<string name="connectivity_type_wifi_only">केवल वै॰फै॰</string>
|
||||
@@ -474,6 +520,7 @@
|
||||
<string name="light">प्रकाशवान</string>
|
||||
<string name="dark">अन्धकारमय</string>
|
||||
<string name="application_preferences">क्रमक आद्यताएँ</string>
|
||||
<string name="wallet_connect">धनकोष संयोजन</string>
|
||||
<string name="language">भाषा</string>
|
||||
<string name="theme">प्रदर्शनशैली</string>
|
||||
<string name="automatically_load_images_gifs">चित्र पूर्वीक्षण</string>
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
<string name="report_explicit_content">Szókimondó tartalom bejelentés</string>
|
||||
<string name="report_illegal_behaviour">Illegális viselkedés bejelentés</string>
|
||||
<string name="report_malware">Vírus bejelentése</string>
|
||||
<string name="report_mod">Jelentés Mod</string>
|
||||
<string name="malware">Vírus</string>
|
||||
<string name="mod">Mod</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Jelentkezzen be privát kulccsal, hogy válaszolni tudjon</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Jelentkezzen be privát kulccsal a bejegyzések megosztásához</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Jelentkezzen be privát kulccsal a bejegyzések kedveléséhez</string>
|
||||
@@ -119,6 +121,8 @@
|
||||
<string name="save_to_gallery">Mentés a galériába</string>
|
||||
<string name="image_saved_to_the_gallery">A kép a galériába mentve</string>
|
||||
<string name="failed_to_save_the_image">Nem sikerült menteni a képet</string>
|
||||
<string name="video_saved_to_the_gallery">A videó a telefon galériájába mentve</string>
|
||||
<string name="failed_to_save_the_video">Nem sikerült a videót menteni</string>
|
||||
<string name="upload_image">Kép feltöltése</string>
|
||||
<string name="uploading">Feltöltés…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">A felhasználó a sat fogadáshoz nem rendelkezik LN cím beállítással</string>
|
||||
@@ -355,8 +359,11 @@
|
||||
<string name="delete_media_server">Médiaszerver törlése</string>
|
||||
<string name="upload_server_relays_nip95">Saját csomópontjaid (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">A fájlokat a csomópontokra töltik fel és ott tárolják. Rögzített URL-től mentesek (harmadik féltől való függőség). Győződj meg róla, hogy legalább egy NIP-95 csomópont a csomópontlistában szerepel</string>
|
||||
<string name="privacy_options">Adatvédelmi Beállítások</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot beállítása</string>
|
||||
<string name="connect_via_tor">Csatlakozás a saját Orbot beállításod alapján</string>
|
||||
<string name="connect_via_tor1">Beállítás</string>
|
||||
<string name="connect_via_tor2">Tor Beállítások</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">Lecsatlakozás a saját Orbot/Tor hálózatról?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Az adataid azonnal a hagyományos hálózaton lesz továbbítva</string>
|
||||
<string name="yes">Igen</string>
|
||||
@@ -365,6 +372,7 @@
|
||||
<string name="follow_list_kind3follows">Mindenki akit követek</string>
|
||||
<string name="follow_list_global">Globális</string>
|
||||
<string name="follow_list_mute_list">Némítottak listája</string>
|
||||
<string name="connect_through_your_orbot_setup_short">Alapértelmezett port a 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Kapcsolódj a TOR-on keresztül az Orbot segítségével
|
||||
\n\n1. Telepítsd az [Orbot-ot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Indítsd el az Orbot-ot
|
||||
@@ -374,6 +382,45 @@
|
||||
\n6. Kattíts az Aktiválás gombra, hogy az Orbot-ot átjátszóként használd
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks portja</string>
|
||||
<string name="use_internal_tor">Aktív Tor motor</string>
|
||||
<string name="use_internal_tor_explainer">Használd a belső verziót vagy az Orbot-ot</string>
|
||||
<string name="tor_preset">Tor/Adatvédelmi Beállítások</string>
|
||||
<string name="tor_preset_explainer">Gyorsan módosítsd az összes alábbi beállítást</string>
|
||||
<string name="tor_use_onion_address">Onion Url/Csomópontok</string>
|
||||
<string name="tor_use_onion_address_explainer">Használd a Tor-t bármely .onion Url-hez</string>
|
||||
<string name="tor_use_dm_relays">PÜ Csomópontok</string>
|
||||
<string name="tor_use_dm_relays_explainer">Tor kényszerítése PÜ küldésére és fogadására</string>
|
||||
<string name="tor_use_new_relays">Megbízhatatlan Csomópontok</string>
|
||||
<string name="tor_use_new_relays_explainer">Tor kényszerítése a kimenő/bejövő üzenetek csomópontjaihoz</string>
|
||||
<string name="tor_use_trusted_relays">Megbízható Csomópontok</string>
|
||||
<string name="tor_use_trusted_relays_explainer">Tor kényszerítése a listákon szereplő összes csomópontra</string>
|
||||
<string name="tor_use_profile_pictures">Profilképek</string>
|
||||
<string name="tor_use_profile_pictures_explainer">Tor kényszerítése profilképek betöltésekor</string>
|
||||
<string name="tor_use_url_previews">URL Előnézetek</string>
|
||||
<string name="tor_use_url_previews_explainer">Tor kényszerítése URL előnézetekor</string>
|
||||
<string name="tor_use_images">Képek</string>
|
||||
<string name="tor_use_images_explainer">Tor kényszerítése képek betöltésekor</string>
|
||||
<string name="tor_use_videos">Videók</string>
|
||||
<string name="tor_use_videos_explainer">Tor kényszerítése videók betöltésekor</string>
|
||||
<string name="tor_use_money_operations">Pénzműveletek</string>
|
||||
<string name="tor_use_money_operations_explainer">Tor kényszerítések zep-re, lightning és cashu tranzakciókhoz</string>
|
||||
<string name="tor_use_nip05_verification">Nostr Cím Ellenőrzése</string>
|
||||
<string name="tor_use_nip05_verification_explainer">Tor kényszerítése a NIP-05 címek ellenőrzésekor</string>
|
||||
<string name="tor_use_nip96_uploads">Média Feltöltések</string>
|
||||
<string name="tor_use_nip96_uploads_explainer">Tor kényszerítése média feltöltésekor</string>
|
||||
<string name="tor_internal">Belső</string>
|
||||
<string name="tor_external">Orbot</string>
|
||||
<string name="tor_off">Ki</string>
|
||||
<string name="tor_when_needed">Alap</string>
|
||||
<string name="tor_default">Alapértelmezett</string>
|
||||
<string name="tor_small_payloads">Minden kivéve Média</string>
|
||||
<string name="tor_full_privacy">Teljes Adatvédelem</string>
|
||||
<string name="tor_custom">Egyéni</string>
|
||||
<string name="tor_when_needed_explainer">Tor használata ha a szerver megköveteli</string>
|
||||
<string name="tor_default_explainer">IP-cím elrejtése a véletlenszerű csomópontok elől</string>
|
||||
<string name="tor_small_payloads_explainer">IP-cím elrejtése mindentől, kivéve a képek- és videóknál</string>
|
||||
<string name="tor_full_privacy_explainer">IP-cím elrejtése minden kapcsolatnál</string>
|
||||
<string name="tor_custom_explainer">Hozd létre a sajátodat</string>
|
||||
<string name="invalid_port_number">Érvénytelen Port szám</string>
|
||||
<string name="use_orbot">Használd az Orbot-ot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">TOR/Orbot Lekapcsolása</string>
|
||||
@@ -459,6 +506,7 @@
|
||||
<string name="community_no_descriptor">Ennek a csoportnak nincsen leírása. A módosítás miatt, a tulajdonossal egyeztess.</string>
|
||||
<string name="add_sensitive_content_label">Érzékeny tartalom</string>
|
||||
<string name="add_sensitive_content_description">Megjelenítés előtt egy érzékeny tartalom figyelmeztetés jelenik meg.</string>
|
||||
<string name="preferences">App Beállítások</string>
|
||||
<string name="settings">Beállítások</string>
|
||||
<string name="connectivity_type_always">Mindig</string>
|
||||
<string name="connectivity_type_wifi_only">Csak WIFI</string>
|
||||
@@ -670,6 +718,7 @@
|
||||
<string name="media_compression_quality_low">Alacsony</string>
|
||||
<string name="media_compression_quality_medium">Közepes</string>
|
||||
<string name="media_compression_quality_high">Magas</string>
|
||||
<string name="media_compression_quality_uncompressed">Tömörítetlen</string>
|
||||
<string name="edit_draft">Piszkozat szerkesztése</string>
|
||||
<string name="login_with_qr_code">Bejelentkezés QR kóddal</string>
|
||||
<string name="route">Útvonal</string>
|
||||
@@ -822,4 +871,5 @@
|
||||
<string name="torrent_failure">Nem sikerült megnyitni a fájlt</string>
|
||||
<string name="torrent_no_apps">A fájl megnyitásához és letöltéséhez nincsenek torrentalkalmazások telepítve.</string>
|
||||
<string name="select_list_to_filter">Válassz egy listát a hírcsatorna szűréséhez</string>
|
||||
<string name="temporary_account">Eszköz zárolása esetén jelentkezzen ki</string>
|
||||
</resources>
|
||||
|
||||
@@ -360,6 +360,7 @@
|
||||
<string name="delete_media_server">Mediaserver verwijderen</string>
|
||||
<string name="upload_server_relays_nip95">Uw relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Bestanden worden geüpload naar en gehost door relays. Ze zijn vrij van een vaste url (afhankelijkheid van derden). Zorg ervoor dat u een NIP-95 relay in uw lijst met relays hebt.</string>
|
||||
<string name="privacy_options">Privacyopties</string>
|
||||
<string name="connect_via_tor_short">Orbot/Tor setup</string>
|
||||
<string name="connect_via_tor">Verbinden via Orbot setup</string>
|
||||
<string name="connect_via_tor1">Aanpassen</string>
|
||||
@@ -506,6 +507,7 @@
|
||||
<string name="community_no_descriptor">Deze community heeft geen beschrijving. Praat met de eigenaar om er een toe te voegen</string>
|
||||
<string name="add_sensitive_content_label">Gevoelige content</string>
|
||||
<string name="add_sensitive_content_description">Voegt waarschuwing voor gevoelige content toe voordat deze content wordt weergegeven</string>
|
||||
<string name="preferences">App-voorkeuren</string>
|
||||
<string name="settings">Instellingen</string>
|
||||
<string name="connectivity_type_always">Altijd</string>
|
||||
<string name="connectivity_type_wifi_only">Alleen wifi</string>
|
||||
@@ -518,6 +520,7 @@
|
||||
<string name="light">Licht</string>
|
||||
<string name="dark">Donker</string>
|
||||
<string name="application_preferences">Application preferences</string>
|
||||
<string name="wallet_connect">Wallet Connect</string>
|
||||
<string name="language">Taal</string>
|
||||
<string name="theme">Thema</string>
|
||||
<string name="automatically_load_images_gifs">Automatisch afbeeldingen/gifs laden</string>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<string name="unknown_author">Autor nieznany</string>
|
||||
<string name="copy_text">Skopiuj tekst</string>
|
||||
<string name="copy_user_pubkey">Kopiuj identyfikator autora</string>
|
||||
<string name="copy_note_id">Kopiuj identyfikator notatki</string>
|
||||
<string name="copy_note_id">Kopiuj identyfikator wpisu</string>
|
||||
<string name="broadcast">Roześlij</string>
|
||||
<string name="timestamp_it">Znacznik czasowy</string>
|
||||
<string name="timestamp_pending">Znacznik czasowy: Oczekujące potwierdzenia</string>
|
||||
@@ -132,7 +132,7 @@
|
||||
<string name="edits_the_channel_metadata">Edytuje metadane kanału</string>
|
||||
<string name="join">Dołącz</string>
|
||||
<string name="known">Znajomi</string>
|
||||
<string name="new_requests">Nowe Prośby</string>
|
||||
<string name="new_requests">Nowe Kontakty</string>
|
||||
<string name="blocked_users">Zablokowani użytkownicy</string>
|
||||
<string name="new_threads">Nowe Wpisy</string>
|
||||
<string name="conversations">Konwersacje</string>
|
||||
@@ -257,7 +257,7 @@
|
||||
<string name="quick_action_share_browser_link">Udostępnij link przeglądarki</string>
|
||||
<string name="quick_action_share">Udostępnij</string>
|
||||
<string name="quick_action_copy_user_id">ID autora</string>
|
||||
<string name="quick_action_copy_note_id">ID notatki</string>
|
||||
<string name="quick_action_copy_note_id">ID wpisu</string>
|
||||
<string name="quick_action_copy_text">Skopiuj tekst</string>
|
||||
<string name="quick_action_delete">Usuń</string>
|
||||
<string name="quick_action_unfollow">Porzuć</string>
|
||||
@@ -265,7 +265,7 @@
|
||||
<string name="quick_action_request_deletion_gallery_title">Usuń z Galerii</string>
|
||||
<string name="quick_action_request_deletion_gallery_alert_body">Usuń te media z galerii, możesz dodać je ponownie później</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Poproś o usunięcie</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst poprosi o usunięcie Twojej notatki z aktualnie podłączonych transmiterów. Nie ma gwarancji, że Twoja notatka zostanie trwale usunięta z tych lub z innych transmiterów, gdzie może być przechowywana.</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst poprosi o usunięcie Twojego wpisu z aktualnie podłączonych transmiterów. Nie ma gwarancji, że Twój wpis zostanie trwale usunięty z tych lub z innych transmiterów, gdzie może być przechowywany.</string>
|
||||
<string name="quick_action_block_dialog_btn">Zablokuj</string>
|
||||
<string name="quick_action_delete_dialog_btn">Usuń</string>
|
||||
<string name="quick_action_block">Zablokuj</string>
|
||||
@@ -278,7 +278,7 @@
|
||||
<string name="report_dialog_nudity">Nagość lub obraźliwa grafika</string>
|
||||
<string name="report_dialog_illegal">Antyspołeczne zachowanie</string>
|
||||
<string name="report_dialog_malware">Złośliwe oprogramowanie</string>
|
||||
<string name="report_dialog_blocking_a_user">Zablokowanie użytkownika ukryje jego zawartość w aplikacji. Twoje notatki są nadal widoczne publicznie, w tym dla osób, które blokujesz. Zablokowani użytkownicy są wymienieni na ekranie filtrów bezpieczeństwa.</string>
|
||||
<string name="report_dialog_blocking_a_user">Zablokowanie użytkownika ukryje jego zawartość w aplikacji. Twoje wpisy będą nadal widoczne publicznie, w tym dla osób, które blokujesz. Zablokowani użytkownicy są wymienieni na ekranie filtrów bezpieczeństwa.</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Zablokuj i ukryj użytkownika]]></string>
|
||||
<string name="report_dialog_report_btn">Zgłoś nadużycie</string>
|
||||
<string name="report_dialog_reminder_public">Wszystkie wysłane zgłoszenia będą publicznie dostępne.</string>
|
||||
@@ -360,8 +360,11 @@
|
||||
<string name="delete_media_server">Usuń serwer multimediów</string>
|
||||
<string name="upload_server_relays_nip95">Twoje transmitery (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Pliki są przechowywane przez Twoje retransmitery. Nowy NIP: sprawdź, czy jest obsługiwany</string>
|
||||
<string name="privacy_options">Ustawienia prywatności</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot - Ustawienia</string>
|
||||
<string name="connect_via_tor">Połącz przez swoją konfigurację Orbota</string>
|
||||
<string name="connect_via_tor1">Dostosuj</string>
|
||||
<string name="connect_via_tor2">Ustawienia TORa</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">Odłączyć od Orbota i Tora?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Twoje dane zostaną natychmiast przekazane w ramach zwykłej sieci</string>
|
||||
<string name="yes">Tak</string>
|
||||
@@ -382,6 +385,8 @@
|
||||
<string name="orbot_socks_port">Port Socks Orbota</string>
|
||||
<string name="use_internal_tor">Aktywny silnik Tor</string>
|
||||
<string name="use_internal_tor_explainer">Użyj wersji wewnętrznej lub Orbota</string>
|
||||
<string name="tor_preset">TOR wstępne ustawienia prywatności</string>
|
||||
<string name="tor_preset_explainer">Szybka modyfikacja wszystkich ustawień poniżej</string>
|
||||
<string name="tor_use_onion_address">Adres Onion /Transmitery</string>
|
||||
<string name="tor_use_onion_address_explainer">Użyj Tor dla dowolnego adresu .onion</string>
|
||||
<string name="tor_use_dm_relays">Transmitery DM</string>
|
||||
@@ -407,6 +412,16 @@
|
||||
<string name="tor_internal">Wewnętrzny</string>
|
||||
<string name="tor_external">Orbot</string>
|
||||
<string name="tor_off">Wył.</string>
|
||||
<string name="tor_when_needed">Podstawowe</string>
|
||||
<string name="tor_default">Domyślne</string>
|
||||
<string name="tor_small_payloads">Wszystko oprócz mediów</string>
|
||||
<string name="tor_full_privacy">Pełna prywatność</string>
|
||||
<string name="tor_custom">Niestandardowy</string>
|
||||
<string name="tor_when_needed_explainer">Użyj Tor, gdy jest wymagany przez serwer</string>
|
||||
<string name="tor_default_explainer">Ukryj swój adres IP przed przypadkowymi transmiterami</string>
|
||||
<string name="tor_small_payloads_explainer">Ukryj swój adres IP od wszystkiego oprócz zdjęć i filmów</string>
|
||||
<string name="tor_full_privacy_explainer">Ukryj swój adres IP przy wszystkich połączeniach</string>
|
||||
<string name="tor_custom_explainer">Stwórz swój własny</string>
|
||||
<string name="invalid_port_number">Nieprawidłowy numer portu</string>
|
||||
<string name="use_orbot">Użyj Orbota</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Odłącz Tor/Orbota</string>
|
||||
@@ -492,6 +507,7 @@
|
||||
<string name="community_no_descriptor">Ta społeczność nie ma opisu. Porozmawiaj z właścicielem, aby go dodać</string>
|
||||
<string name="add_sensitive_content_label">Treść wrażliwa</string>
|
||||
<string name="add_sensitive_content_description">Dodaje ostrzeżenie o wrażliwej treści przed wyświetleniem tej zawartości</string>
|
||||
<string name="preferences">Ustawienia Aplikacji</string>
|
||||
<string name="settings">Ustawienia</string>
|
||||
<string name="connectivity_type_always">Zawsze</string>
|
||||
<string name="connectivity_type_wifi_only">Tylko WiFi</string>
|
||||
@@ -504,6 +520,7 @@
|
||||
<string name="light">Jasny</string>
|
||||
<string name="dark">Ciemny</string>
|
||||
<string name="application_preferences">Ustawienia Aplikacji</string>
|
||||
<string name="wallet_connect">Podłącz portfel</string>
|
||||
<string name="language">Język</string>
|
||||
<string name="theme">Motyw</string>
|
||||
<string name="automatically_load_images_gifs">Podgląd obrazu</string>
|
||||
|
||||
@@ -358,6 +358,7 @@
|
||||
<string name="delete_media_server">Apagar servidor de mídia</string>
|
||||
<string name="upload_server_relays_nip95">Seus relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Os arquivos são hospedados por seus relays. Novo NIP: verifique se eles suportam</string>
|
||||
<string name="privacy_options">Opções de Privacidade</string>
|
||||
<string name="connect_via_tor_short">Configuração do Tor/Orbot</string>
|
||||
<string name="connect_via_tor">Conecte-se através da configuração do Orbot</string>
|
||||
<string name="connect_via_tor1">Ajustar</string>
|
||||
@@ -407,6 +408,7 @@
|
||||
<string name="tor_use_nip96_uploads">Envio de Mídia</string>
|
||||
<string name="tor_use_nip96_uploads_explainer">Forçar Tor ao enviar conteúdo</string>
|
||||
<string name="tor_internal">Interno</string>
|
||||
<string name="tor_external">Orbot</string>
|
||||
<string name="tor_off">Desativado</string>
|
||||
<string name="tor_when_needed">Básico</string>
|
||||
<string name="tor_default">Padrão</string>
|
||||
@@ -503,6 +505,7 @@
|
||||
<string name="community_no_descriptor">Esta comunidade não tem uma descrição. Fale com o proprietário para adicionar</string>
|
||||
<string name="add_sensitive_content_label">Conteúdo sensível</string>
|
||||
<string name="add_sensitive_content_description">Adiciona aviso de conteúdo sensível antes de mostrar este conteúdo</string>
|
||||
<string name="preferences">Preferências do aplicativo</string>
|
||||
<string name="settings">Configurações</string>
|
||||
<string name="connectivity_type_always">Sempre</string>
|
||||
<string name="connectivity_type_wifi_only">Somente wifi</string>
|
||||
@@ -515,6 +518,7 @@
|
||||
<string name="light">Claro</string>
|
||||
<string name="dark">Escuro</string>
|
||||
<string name="application_preferences">Preferências do aplicativo</string>
|
||||
<string name="wallet_connect">Conectar Carteira</string>
|
||||
<string name="language">Linguagem</string>
|
||||
<string name="theme">Tema</string>
|
||||
<string name="automatically_load_images_gifs">Carregar imagens/gifs automaticamente</string>
|
||||
|
||||
@@ -5,4 +5,22 @@
|
||||
Prijavi se s privatnim ključem za všečkanje sporočila</string>
|
||||
<string name="view_count">Števec vpogledov</string>
|
||||
<string name="following">""</string>
|
||||
<string name="home_feed"></string>
|
||||
<string name="public_chat_feed">Javni vir novic</string>
|
||||
<string name="add_a_relay">Dodaj rele</string>
|
||||
<string name="display_name">Prikazno ime</string>
|
||||
<string name="uploading">Nalaganje …</string>
|
||||
<string name="npub_hex_username">"npub, uporabniško ime, tekst"</string>
|
||||
<string name="quick_action_share_browser_link">Deli povezavo brskalnika</string>
|
||||
<string name="quick_action_request_deletion_gallery_title">Izbris iz galerije</string>
|
||||
<string name="quick_action_block_dialog_btn">Blokiraj</string>
|
||||
<string name="quick_action_delete_button">Izbriši</string>
|
||||
<string name="quick_action_dont_show_again_button">Ne prikaži ponovno</string>
|
||||
<string name="report_dialog_select_reason_label">Razlog</string>
|
||||
<string name="poll_closing_time">Zapri po</string>
|
||||
<string name="follow_list_global">Globalno</string>
|
||||
<string name="tor_use_profile_pictures_explainer">Prisili tor pri nalaganju profilnih slik</string>
|
||||
<string name="ui_feature_set_type_complete">Končaj</string>
|
||||
<string name="mute_button">Zvok je prižgan. Klikni da ga utišaš</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">Neuspela povezava z %1$s: %2$s</string>
|
||||
</resources>
|
||||
|
||||
@@ -358,6 +358,7 @@
|
||||
<string name="delete_media_server">Radera mediaserver</string>
|
||||
<string name="upload_server_relays_nip95">Dina Reläer (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Filer är värd för dina reläer. Nytt NIP: kontrollera om de stöder</string>
|
||||
<string name="privacy_options">Sekretess Alternativ</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot Inställningar</string>
|
||||
<string name="connect_via_tor">Anslut via dina Orbot-inställiningar</string>
|
||||
<string name="connect_via_tor1">Justera</string>
|
||||
@@ -406,6 +407,7 @@
|
||||
<string name="tor_use_nip96_uploads">Mediauppladdningar</string>
|
||||
<string name="tor_use_nip96_uploads_explainer">Tvinga Tor vid uppladdning av innehåll</string>
|
||||
<string name="tor_internal">Intern</string>
|
||||
<string name="tor_external">Orbot</string>
|
||||
<string name="tor_off">Av</string>
|
||||
<string name="tor_when_needed">Grundläggande</string>
|
||||
<string name="tor_default">Standard</string>
|
||||
@@ -502,6 +504,7 @@
|
||||
<string name="community_no_descriptor">Denna gemenskap har ingen beskrivning. Prata med ägaren för att lägga till en.</string>
|
||||
<string name="add_sensitive_content_label">Känsligt innehåll</string>
|
||||
<string name="add_sensitive_content_description">Lägger till varning för känsligt innehåll innan detta innehåll visas.</string>
|
||||
<string name="preferences">Appinställningar</string>
|
||||
<string name="settings">Inställningar</string>
|
||||
<string name="connectivity_type_always">Alltid</string>
|
||||
<string name="connectivity_type_wifi_only">Endast Wi-Fi</string>
|
||||
@@ -514,6 +517,7 @@
|
||||
<string name="light">Ljus</string>
|
||||
<string name="dark">Mörk</string>
|
||||
<string name="application_preferences">Applikationsinställningar</string>
|
||||
<string name="wallet_connect">Plånboksanslut</string>
|
||||
<string name="language">Språk</string>
|
||||
<string name="theme">Tema</string>
|
||||
<string name="automatically_load_images_gifs">Ladda automatiskt bilder/gif</string>
|
||||
|
||||
@@ -365,6 +365,7 @@
|
||||
<string name="follow_list_kind3follows">ติดตามทั้งหมด</string>
|
||||
<string name="follow_list_global">ทั่วโลก</string>
|
||||
<string name="follow_list_mute_list">รายการผู้ถูกปิดกั้น</string>
|
||||
<string name="connect_through_your_orbot_setup_short">Port เริ่มต้นคือ 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## เชื่อมต่อกัย Tor ด้วย Orbot
|
||||
\n\n1. ติดตั้ง [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. เริ่มต้นใช้งาน Orbot
|
||||
@@ -374,6 +375,8 @@
|
||||
\n6. กดปุ่มเปิดใช้งานเพื่อใช้ Orbot เป็น proxy
|
||||
</string>
|
||||
<string name="orbot_socks_port">พอร์ต Orbot Socks</string>
|
||||
<string name="tor_off">ปิด</string>
|
||||
<string name="tor_custom">ปรับแต่ง</string>
|
||||
<string name="invalid_port_number">เลข port ไม่ถูกต้องr</string>
|
||||
<string name="use_orbot">ใช้ Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">ตัดการเชื่อมต่อจาก Tor/Orbot</string>
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
<string name="save_to_gallery">保存到相册</string>
|
||||
<string name="image_saved_to_the_gallery">图片已保存到相册</string>
|
||||
<string name="failed_to_save_the_image">保存图片失败</string>
|
||||
<string name="video_saved_to_the_gallery">视频已保存到媒体库</string>
|
||||
<string name="failed_to_save_the_video">保存视频失败</string>
|
||||
<string name="upload_image">上传图片</string>
|
||||
<string name="uploading">上传中…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">用户尚未设置闪电地址以接收聪</string>
|
||||
@@ -358,8 +360,11 @@
|
||||
<string name="delete_media_server">删除媒体服务器</string>
|
||||
<string name="upload_server_relays_nip95">你的中继器 (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">文件由你的中继器托管。新的 NIP:检查它们是否支持</string>
|
||||
<string name="privacy_options">隐私选项</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot 设置</string>
|
||||
<string name="connect_via_tor">通过你的 Orbot 设置连接</string>
|
||||
<string name="connect_via_tor1">调整</string>
|
||||
<string name="connect_via_tor2">Tor 设置</string>
|
||||
<string name="do_you_really_want_to_disable_tor_title">断开与你的 Orbot/Tor 连接?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">你的数据将立即在普通网络上传输</string>
|
||||
<string name="yes">是</string>
|
||||
@@ -368,6 +373,7 @@
|
||||
<string name="follow_list_kind3follows">所有关注</string>
|
||||
<string name="follow_list_global">全球</string>
|
||||
<string name="follow_list_mute_list">静音列表</string>
|
||||
<string name="connect_through_your_orbot_setup_short">默认端口为 9050</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## 通过 Orbot 连线 Tor
|
||||
\n\n1. 安装 [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. 开启 Orbot
|
||||
@@ -377,6 +383,45 @@
|
||||
\n6. 按“启用”按钮以使用 Orbot 作为代理
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks 端口</string>
|
||||
<string name="use_internal_tor">启动 Tor</string>
|
||||
<string name="use_internal_tor_explainer">使用内置的版本或者 Orbot</string>
|
||||
<string name="tor_preset">Tor 和隐私预设</string>
|
||||
<string name="tor_preset_explainer">快速修改下方的设置</string>
|
||||
<string name="tor_use_onion_address">Onion 链接或中继地址</string>
|
||||
<string name="tor_use_onion_address_explainer">为 .onion 链接使用 Tor</string>
|
||||
<string name="tor_use_dm_relays">私信中继</string>
|
||||
<string name="tor_use_dm_relays_explainer">强制在收发私信时使用 Tor</string>
|
||||
<string name="tor_use_new_relays">不受信任的中继</string>
|
||||
<string name="tor_use_new_relays_explainer">强制对收件箱和发件箱中继使用 Tor</string>
|
||||
<string name="tor_use_trusted_relays">可信中继</string>
|
||||
<string name="tor_use_trusted_relays_explainer">强制为所有中继使用 Tor</string>
|
||||
<string name="tor_use_profile_pictures">用户资料图片</string>
|
||||
<string name="tor_use_profile_pictures_explainer">强制为下载用户资料图片时使用 Tor</string>
|
||||
<string name="tor_use_url_previews">链接预览</string>
|
||||
<string name="tor_use_url_previews_explainer">强制为链接预览使用 Tor</string>
|
||||
<string name="tor_use_images">图片</string>
|
||||
<string name="tor_use_images_explainer">强制为加载图片时使用 Tor</string>
|
||||
<string name="tor_use_videos">视频</string>
|
||||
<string name="tor_use_videos_explainer">强制为加载视频时使用 Tor</string>
|
||||
<string name="tor_use_money_operations">资金操作</string>
|
||||
<string name="tor_use_money_operations_explainer">强制为 Zap、闪电网络和 Cashu 转账时使用 Tor</string>
|
||||
<string name="tor_use_nip05_verification">Nostr 地址验证</string>
|
||||
<string name="tor_use_nip05_verification_explainer">强制为 NIP-05 地址验证使用 Tor</string>
|
||||
<string name="tor_use_nip96_uploads">媒体上传</string>
|
||||
<string name="tor_use_nip96_uploads_explainer">强制为上传媒体内容时使用 Tor</string>
|
||||
<string name="tor_internal">内置</string>
|
||||
<string name="tor_external">Orbot</string>
|
||||
<string name="tor_off">关闭</string>
|
||||
<string name="tor_when_needed">基础</string>
|
||||
<string name="tor_default">默认</string>
|
||||
<string name="tor_small_payloads">除多媒体外的所有内容</string>
|
||||
<string name="tor_full_privacy">完全隐私</string>
|
||||
<string name="tor_custom">自定义</string>
|
||||
<string name="tor_when_needed_explainer">当服务器要求时使用 Tor</string>
|
||||
<string name="tor_default_explainer">从随机中继中隐藏 IP</string>
|
||||
<string name="tor_small_payloads_explainer">除了图片和视频外,总是隐藏 IP</string>
|
||||
<string name="tor_full_privacy_explainer">为所有连接隐藏 IP</string>
|
||||
<string name="tor_custom_explainer">让你自己来设置</string>
|
||||
<string name="invalid_port_number">无效端口</string>
|
||||
<string name="use_orbot">使用 Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">断开 Tor/Orbot 连接</string>
|
||||
@@ -462,6 +507,7 @@
|
||||
<string name="community_no_descriptor">此社区没有描述或规则。联系群主来添加</string>
|
||||
<string name="add_sensitive_content_label">敏感内容</string>
|
||||
<string name="add_sensitive_content_description">在显示此内容之前添加敏感内容警告</string>
|
||||
<string name="preferences">偏好设置</string>
|
||||
<string name="settings">设置</string>
|
||||
<string name="connectivity_type_always">始终</string>
|
||||
<string name="connectivity_type_wifi_only">仅限 WiFi</string>
|
||||
|
||||
@@ -401,6 +401,7 @@
|
||||
<string name="upload_server_relays_nip95">Your relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Files are hosted by your relays. New NIP: check if they support</string>
|
||||
|
||||
<string name="privacy_options">Privacy Options</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot setup</string>
|
||||
<string name="connect_via_tor">Connect through your Orbot setup</string>
|
||||
|
||||
@@ -592,6 +593,9 @@
|
||||
|
||||
<string name="add_sensitive_content_label">Sensitive Content</string>
|
||||
<string name="add_sensitive_content_description">Adds sensitive content warning before showing this content</string>
|
||||
|
||||
<string name="preferences">App Preferences</string>
|
||||
|
||||
<string name="settings">Settings</string>
|
||||
<string name="connectivity_type_always">Always</string>
|
||||
<string name="connectivity_type_wifi_only">Wifi-only</string>
|
||||
@@ -606,6 +610,7 @@
|
||||
<string name="light">Light</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="application_preferences">Application Preferences</string>
|
||||
<string name="wallet_connect">Wallet Connect</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="automatically_load_images_gifs">Image Preview</string>
|
||||
|
||||
+6
-5
@@ -46,6 +46,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
@@ -230,7 +231,7 @@ private fun TranslationMessage(
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (source in accountViewModel.account.settings.dontTranslateFrom) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
@@ -252,14 +253,14 @@ private fun TranslationMessage(
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
accountViewModel.account.settings.addDontTranslateFrom(source)
|
||||
accountViewModel.account.settings.toggleDontTranslateFrom(source)
|
||||
langSettingsPopupExpanded = false
|
||||
},
|
||||
)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (accountViewModel.account.settings.preferenceBetween(source, target) == source) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
@@ -289,7 +290,7 @@ private fun TranslationMessage(
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (accountViewModel.account.settings.preferenceBetween(source, target) == target) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
@@ -324,7 +325,7 @@ private fun TranslationMessage(
|
||||
languageList.get(i)?.let { lang ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (accountViewModel.account.settings.translateToContains(lang)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Looper
|
||||
import com.abedelazizshe.lightcompressorlibrary.VideoCompressor
|
||||
import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils
|
||||
import id.zelory.compressor.Compressor
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
class MediaCompressorTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
// Mock compressors
|
||||
mockkStatic(VideoCompressor::class)
|
||||
mockkObject(Compressor)
|
||||
|
||||
// mock out main thread check
|
||||
mockkStatic(Looper::class)
|
||||
every { Looper.myLooper() } returns mockk<Looper>()
|
||||
every { Looper.getMainLooper() } returns mockk<Looper>()
|
||||
MockKAnnotations.init(this)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Compression level uncompressed should not compress media`() =
|
||||
runTest {
|
||||
// setup
|
||||
val mediaQuality = CompressorQuality.UNCOMPRESSED
|
||||
val uri = mockk<Uri>()
|
||||
val contentType = "video"
|
||||
|
||||
// Execution
|
||||
MediaCompressor().compress(
|
||||
uri,
|
||||
contentType,
|
||||
applicationContext = mockk(),
|
||||
onReady = { _, _, _ -> },
|
||||
onError = { },
|
||||
mediaQuality = mediaQuality,
|
||||
)
|
||||
|
||||
// Verify
|
||||
verify(exactly = 0) { VideoCompressor.start(any(), any(), any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) { Compressor.compress(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Unknown media type should be skipped`() =
|
||||
runTest {
|
||||
// setup
|
||||
val mediaQuality = CompressorQuality.MEDIUM
|
||||
val uri = mockk<Uri>()
|
||||
val contentType = "test"
|
||||
|
||||
// Execution
|
||||
MediaCompressor().compress(
|
||||
uri,
|
||||
contentType,
|
||||
applicationContext = mockk(),
|
||||
onReady = { _, _, _ -> },
|
||||
onError = { },
|
||||
mediaQuality = mediaQuality,
|
||||
)
|
||||
|
||||
// Verify
|
||||
verify(exactly = 0) { VideoCompressor.start(any(), any(), any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) { Compressor.compress(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Video media should invoke video compressor`() =
|
||||
runTest {
|
||||
// setup
|
||||
val mediaQuality = CompressorQuality.MEDIUM
|
||||
val uri = mockk<Uri>()
|
||||
val contentType = "video"
|
||||
|
||||
every { VideoCompressor.start(any(), any(), any(), any(), any(), any(), any()) } returns Unit
|
||||
|
||||
// Execution
|
||||
MediaCompressor().compress(
|
||||
uri,
|
||||
contentType,
|
||||
applicationContext = mockk(),
|
||||
onReady = { _, _, _ -> },
|
||||
onError = { },
|
||||
mediaQuality = mediaQuality,
|
||||
)
|
||||
|
||||
// Verify
|
||||
verify(exactly = 1) { VideoCompressor.start(any(), any(), any(), any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Image media should invoke image compressor`() =
|
||||
runTest {
|
||||
// setup
|
||||
val mediaQuality = CompressorQuality.MEDIUM
|
||||
val uri = mockk<Uri>()
|
||||
val contentType = "image"
|
||||
|
||||
mockkObject(MediaCompressorFileUtils)
|
||||
every { MediaCompressorFileUtils.from(any(), any()) } returns File("test")
|
||||
coEvery { Compressor.compress(any(), any(), any(), any()) } returns File("test")
|
||||
|
||||
// Execution
|
||||
MediaCompressor().compress(
|
||||
uri,
|
||||
contentType,
|
||||
applicationContext = mockk<Context>(relaxed = true),
|
||||
onReady = { _, _, _ -> },
|
||||
onError = { },
|
||||
mediaQuality = mediaQuality,
|
||||
)
|
||||
|
||||
// Verify
|
||||
coVerify(exactly = 1) { Compressor.compress(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Image compression should return back same uri on exception`() =
|
||||
runTest {
|
||||
// setup
|
||||
val mockContext = mockk<Context>(relaxed = true)
|
||||
val mockUri = mockk<Uri>()
|
||||
val mockOnReady = mockk<(Uri, String?, Long?) -> Unit>(relaxed = true)
|
||||
|
||||
mockkObject(MediaCompressorFileUtils)
|
||||
every { MediaCompressorFileUtils.from(any(), any()) } returns File("test")
|
||||
coEvery { Compressor.compress(any(), any<File>(), any(), any()) } throws Exception("Compression error")
|
||||
|
||||
// Execute
|
||||
MediaCompressor().compress(
|
||||
uri = mockUri,
|
||||
contentType = "image/jpeg",
|
||||
applicationContext = mockContext,
|
||||
onReady = mockOnReady,
|
||||
onError = { },
|
||||
mediaQuality = CompressorQuality.MEDIUM,
|
||||
)
|
||||
|
||||
// Verify: onReady should be called with original uri, content type, and null size
|
||||
verify { mockOnReady.invoke(mockUri, "image/jpeg", null) }
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ class Relay(
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendOutbox() {
|
||||
fun sendOutbox() {
|
||||
synchronized(outboxCache) {
|
||||
outboxCache.values.forEach {
|
||||
send(it)
|
||||
|
||||
@@ -96,9 +96,8 @@ object RelayPool : Relay.Listener {
|
||||
addRelay(relay)
|
||||
|
||||
relay.connectAndRun {
|
||||
Client.allSubscriptions().forEach {
|
||||
relay.sendFilter(it.key, it.value)
|
||||
}
|
||||
relay.renewFilters()
|
||||
relay.sendOutbox()
|
||||
|
||||
onConnected(relay)
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
[versions]
|
||||
accompanistAdaptive = "0.34.0"
|
||||
activityCompose = "1.9.2"
|
||||
agp = "8.6.1"
|
||||
android-compileSdk = "34"
|
||||
agp = "8.7.1"
|
||||
android-compileSdk = "35"
|
||||
android-minSdk = "26"
|
||||
android-targetSdk = "34"
|
||||
android-targetSdk = "35"
|
||||
androidKotlinGeohash = "1.0"
|
||||
androidxJunit = "1.2.1"
|
||||
appcompat = "1.7.0"
|
||||
audiowaveform = "1.1.1"
|
||||
benchmark = "1.3.1"
|
||||
benchmarkJunit4 = "1.3.1"
|
||||
benchmark = "1.3.2"
|
||||
benchmarkJunit4 = "1.3.2"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
blurhash = "1.0.0"
|
||||
coil = "2.7.0"
|
||||
composeBom = "2024.09.02"
|
||||
composeBom = "2024.09.03"
|
||||
coreKtx = "1.13.1"
|
||||
espressoCore = "3.6.1"
|
||||
firebaseBom = "33.3.0"
|
||||
fragmentKtx = "1.8.3"
|
||||
firebaseBom = "33.4.0"
|
||||
fragmentKtx = "1.8.4"
|
||||
gms = "4.4.2"
|
||||
jacksonModuleKotlin = "2.17.2"
|
||||
jna = "5.14.0"
|
||||
@@ -35,7 +35,8 @@ lightcompressor = "1.3.2"
|
||||
markdown = "077a2cde64"
|
||||
media3 = "1.4.1"
|
||||
mockk = "1.13.12"
|
||||
navigationCompose = "2.8.1"
|
||||
kotlinx-coroutines-test = "1.9.0-RC.2"
|
||||
navigationCompose = "2.8.2"
|
||||
okhttp = "5.0.0-alpha.14"
|
||||
runner = "1.6.2"
|
||||
rfc3986 = "0.1.0"
|
||||
@@ -116,6 +117,7 @@ markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", nam
|
||||
markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui", version.ref = "markdown" }
|
||||
markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" }
|
||||
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
|
||||
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"}
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
rfc3986-normalizer = { group = "org.czeal", name = "rfc3986", version.ref = "rfc3986" }
|
||||
secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" }
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#Wed Jan 04 09:23:50 EST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -58,6 +58,7 @@ class Permission(
|
||||
class Result(
|
||||
@JsonProperty("package") val `package`: String?,
|
||||
@JsonProperty("signature") val signature: String?,
|
||||
@JsonProperty("result") val result: String?,
|
||||
@JsonProperty("id") val id: String?,
|
||||
) {
|
||||
companion object {
|
||||
@@ -76,7 +77,8 @@ class Result(
|
||||
val jsonObject: JsonNode = jp.codec.readTree(jp)
|
||||
return Result(
|
||||
jsonObject.get("package").asText().intern(),
|
||||
jsonObject.get("signature").asText().intern(),
|
||||
jsonObject.get("signature")?.asText()?.intern(),
|
||||
jsonObject.get("result")?.asText()?.intern(),
|
||||
jsonObject.get("id").asText().intern(),
|
||||
)
|
||||
}
|
||||
@@ -120,7 +122,7 @@ class ExternalSignerLauncher(
|
||||
if (results != null) {
|
||||
val localResults: Array<Result> = Result.fromJsonArray(results)
|
||||
localResults.forEach {
|
||||
val signature = it.signature ?: ""
|
||||
val signature = it.result ?: it.signature ?: ""
|
||||
val packageName = it.`package`?.let { "-$it" } ?: ""
|
||||
val id = it.id ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
@@ -130,7 +132,7 @@ class ExternalSignerLauncher(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val signature = data.getStringExtra("signature") ?: ""
|
||||
val signature = data.getStringExtra("result") ?: data.getStringExtra("signature") ?: ""
|
||||
val packageName = data.getStringExtra("package")?.let { "-$it" } ?: ""
|
||||
val id = data.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
@@ -239,13 +241,11 @@ class ExternalSignerLauncher(
|
||||
|
||||
fun openSigner(
|
||||
event: EventInterface,
|
||||
columnName: String = "signature",
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
getDataFromResolver(
|
||||
SignerType.SIGN_EVENT,
|
||||
arrayOf(event.toJson(), event.pubKey()),
|
||||
columnName,
|
||||
).fold(
|
||||
onFailure = { },
|
||||
onSuccess = {
|
||||
@@ -267,13 +267,11 @@ class ExternalSignerLauncher(
|
||||
private fun getDataFromResolver(
|
||||
signerType: SignerType,
|
||||
data: Array<out String>,
|
||||
columnName: String = "signature",
|
||||
): kotlin.Result<String?> = getDataFromResolver(signerType, data, columnName, contentResolver)
|
||||
): kotlin.Result<String?> = getDataFromResolver(signerType, data, contentResolver)
|
||||
|
||||
private fun getDataFromResolver(
|
||||
signerType: SignerType,
|
||||
data: Array<out String>,
|
||||
columnName: String = "signature",
|
||||
contentResolver: (() -> ContentResolver)? = null,
|
||||
): kotlin.Result<String?> {
|
||||
val localData =
|
||||
@@ -301,9 +299,12 @@ class ExternalSignerLauncher(
|
||||
Log.d("getDataFromResolver", "Permission denied")
|
||||
return kotlin.Result.failure(Exception("Permission denied"))
|
||||
}
|
||||
val index = it.getColumnIndex(columnName)
|
||||
var index = it.getColumnIndex("result")
|
||||
if (index < 0) {
|
||||
Log.d("getDataFromResolver", "column '$columnName' not found")
|
||||
index = it.getColumnIndex("signature")
|
||||
}
|
||||
if (index < 0) {
|
||||
Log.d("getDataFromResolver", "column 'signature' not found")
|
||||
return kotlin.Result.success(null)
|
||||
}
|
||||
return kotlin.Result.success(it.getString(index))
|
||||
|
||||
+2
-3
@@ -2,20 +2,19 @@ amethyst:
|
||||
android:
|
||||
identifier: com.vitorpamplona.amethyst
|
||||
name: Amethyst
|
||||
description: A Nostr client for Android
|
||||
description: The all-in-one Nostr client
|
||||
license: MIT
|
||||
builder: npub142gywvjkq0dv6nupggyn2euhx4nduwc7yz5f24ah9rpmunr2s39se3xrj0
|
||||
repository: https://github.com/vitorpamplona/amethyst
|
||||
artifacts:
|
||||
- amethyst-fdroid-arm64-v8a-v%v.apk
|
||||
- amethyst-fdroid-armeabi-v7a-v%v.apk
|
||||
- amethyst-fdroid-universal-v%v.apk
|
||||
- amethyst-fdroid-v%v.aab
|
||||
- amethyst-fdroid-x86-v%v.apk
|
||||
- amethyst-fdroid-x86_64-v%v.apk
|
||||
- amethyst-googleplay-arm64-v8a-v%v.apk
|
||||
- amethyst-googleplay-armeabi-v7a-v%v.apk
|
||||
- amethyst-googleplay-universal-v%v.apk
|
||||
- amethyst-googleplay-v%v.aab
|
||||
- amethyst-googleplay-x86-v%v.apk
|
||||
- amethyst-googleplay-x86_64-v%v.apk
|
||||
|
||||
|
||||
Reference in New Issue
Block a user