Porównaj commity

...

45 Commity

Autor SHA1 Wiadomość Data
Crowdin Bot b28546b172 New Crowdin translations by GitHub Action 2024-04-12 21:05:53 +00:00
Vitor Pamplona 0fccfd7f80 clean up 2024-04-12 11:02:25 -04:00
Vitor Pamplona 3f35b57571 Improves Zap error messages to include the lnaddress of the error 2024-04-12 11:02:17 -04:00
Vitor Pamplona 32b9b6c37a Speeds up id calculations for Amber's Intent call.
Only assembles an id if necessary.
2024-04-12 11:01:32 -04:00
Vitor Pamplona 2342da114d Increases Amber signing cache. 2024-04-12 11:00:16 -04:00
Vitor Pamplona ef0d77f8eb Removes hardcoded amber packages as default 2024-04-12 10:59:59 -04:00
Vitor Pamplona 5559b69bdb Displays Zap Split error messages in sequence instead of in multiple popups. 2024-04-12 10:58:32 -04:00
Vitor Pamplona eda25b4cfe Callback uri is not necessary 2024-04-12 10:57:33 -04:00
Vitor Pamplona 9ce14e08fd Fixes a bug with signature-null in the `sig` of events. 2024-04-12 10:56:38 -04:00
Vitor Pamplona b046fd91cb Only logs external signer calls on debug 2024-04-11 18:30:28 -04:00
Vitor Pamplona 8c9800664f v0.86.5 2024-04-11 18:20:48 -04:00
Vitor Pamplona b90a57220d Enables Mutiny Wallet NWC 2024-04-11 18:18:54 -04:00
Vitor Pamplona d16b0f58bb Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-11 18:04:53 -04:00
Vitor Pamplona a538b66db3 Removes the use of DM relays to find events due to private inbox settings 2024-04-11 18:04:02 -04:00
Vitor Pamplona f04631b0dd Adds vertical scrolling on the Zap page for collaborators. 2024-04-11 18:01:37 -04:00
Vitor Pamplona 6e31cff99c Avoids decrypting existing Nostr events just to add the relay into the relay list. 2024-04-11 18:01:22 -04:00
Vitor Pamplona 1553640c18 Calculates hash in the IO thread from Compose's scope. 2024-04-11 16:34:34 -04:00
Vitor Pamplona 68b8f9c82a
Merge pull request #834 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-11 13:49:46 -04:00
Crowdin Bot f6cce42028 New Crowdin translations by GitHub Action 2024-04-11 17:47:21 +00:00
Vitor Pamplona 0cbddad9c0 v0.86.4 2024-04-11 13:45:27 -04:00
Vitor Pamplona b14154e2b5
Merge pull request #832 from greenart7c3/main
Do not use tor proxy when localhost, fix proxy not being used inside ImageDownloader.kt
2024-04-10 17:30:51 -04:00
greenart7c3 c4250ccd35 fix error when not using proxy 2024-04-10 14:11:11 -03:00
greenart7c3 31516964c8 Do not use tor proxy when localhost, fix proxy not being used inside ImageDownloader.kt 2024-04-10 13:51:48 -03:00
Vitor Pamplona 4722b2a617 Fixes missing Zaps and some DMs on startup 2024-04-10 11:05:21 -04:00
Vitor Pamplona eca5b47ab0 Updates Kotlin version 2024-04-10 10:20:33 -04:00
Vitor Pamplona d38b57025c Updates compose BOM 2024-04-10 09:00:57 -04:00
Vitor Pamplona fa7aa3cf24 Updates AGP 2024-04-10 08:47:05 -04:00
Vitor Pamplona d8e9b4773b v0.86.3 2024-04-09 19:41:22 -04:00
Vitor Pamplona f9a7b13ba1 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-09 19:36:49 -04:00
Vitor Pamplona ecbf0e404d Fixes missing notifications 2024-04-09 19:36:40 -04:00
Vitor Pamplona c2f8df963a
Merge pull request #831 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-09 12:23:35 -04:00
Crowdin Bot ff20960bb5 New Crowdin translations by GitHub Action 2024-04-09 16:20:30 +00:00
Vitor Pamplona 8dd1fc2077 Fixing F-droid's Markdown use 2024-04-09 12:18:14 -04:00
Vitor Pamplona bb2fb2b103 Fixes max width of hidden notes 2024-04-09 12:15:27 -04:00
Vitor Pamplona 00a9c49915 - Migrates to the new Markdown Parser.
- Adds Note previews on Markdown
- Adds Custom hashtag icons to markdown.
- Adds URL preview boxes to markdown
- Performance improvements.
2024-04-08 18:53:55 -04:00
Vitor Pamplona d2872cc8bb Refactors the url preview state 2024-04-08 18:34:10 -04:00
Vitor Pamplona d33a1ce14f Fixes clickable route not showing the user 's npub before loading the name. 2024-04-08 18:33:50 -04:00
Vitor Pamplona 31958215be Moves loading of an embed event to thread. 2024-04-08 18:33:31 -04:00
Vitor Pamplona bbbb614718 Moves added charts after NIP19 uris to being nullable. 2024-04-08 18:32:37 -04:00
Vitor Pamplona 776a23c256 Clearing the secondary button design or the encrypted nsec backup 2024-04-08 11:34:33 -04:00
Vitor Pamplona 0854bd34ff v0.86.2 2024-04-05 14:08:27 -04:00
Vitor Pamplona 1738a775ef Fixes the lack of the Comparator interface for the Deletion Index. 2024-04-05 14:05:04 -04:00
Vitor Pamplona a6953872ea Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-05 11:39:34 -04:00
Vitor Pamplona d48714456c Fixes check for Deleted Addressable events. 2024-04-05 11:34:50 -04:00
Vitor Pamplona c25aad482b
Merge pull request #829 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-05 11:23:19 -04:00
46 zmienionych plików z 1050 dodań i 745 usunięć

Wyświetl plik

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" />
<option name="version" value="1.9.23" />
</component>
</project>

Wyświetl plik

@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 364
versionName "0.86.1"
versionCode 368
versionName "0.86.5"
buildConfigField "String", "RELEASE_NOTES_ID", "\"a704a11334ed4fe6fc6ee6f8856f6f005da33644770616f1437f8b2b488b52b1\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -143,7 +143,7 @@ android {
composeOptions {
// Should match compose version : https://developer.android.com/jetpack/androidx/releases/compose-kotlin
kotlinCompilerExtensionVersion "1.5.8"
kotlinCompilerExtensionVersion "1.5.11"
}
packagingOptions {
resources {
@ -151,7 +151,6 @@ android {
}
}
lint {
disable 'MissingTranslation'
}

Wyświetl plik

@ -47,9 +47,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.commonmark.MarkdownParseOptions
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.Material3RichText
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler
@ -101,12 +103,18 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
onDismissRequest = { distributorPresent = true },
title = { Text(stringResource(R.string.push_server_install_app)) },
text = {
Material3RichText(
val content = stringResource(R.string.push_server_install_app_description)
val astNode =
remember {
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
}
RichText(
style = RichTextStyle().resolveDefaults(),
renderer = null,
) {
Markdown(
content = stringResource(R.string.push_server_install_app_description),
)
BasicMarkdown(astNode)
}
},
confirmButton = {

Wyświetl plik

@ -171,6 +171,22 @@ object LocalCache {
return channels.get(key)
}
fun getNoteIfExists(event: Event): Note? {
return if (event is AddressableEvent) {
getAddressableNoteIfExists(event.addressTag())
} else {
getNoteIfExists(event.id)
}
}
fun getOrCreateNote(event: Event): Note {
return if (event is AddressableEvent) {
getOrCreateAddressableNote(event.address())
} else {
getOrCreateNote(event.id)
}
}
fun checkGetOrCreateNote(key: String): Note? {
checkNotInMainThread()
@ -959,53 +975,53 @@ object LocalCache {
}
fun consume(event: DeletionEvent) {
deletionIndex.add(event)
if (deletionIndex.add(event)) {
var deletedAtLeastOne = false
var deletedAtLeastOne = false
event.deleteEvents()
.mapNotNull { getNoteIfExists(it) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey) {
// reverts the add
deleteNote(deleteNote)
event.deleteEvents()
.mapNotNull { getNoteIfExists(it) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey) {
// reverts the add
deleteNote(deleteNote)
deletedAtLeastOne = true
}
}
deletedAtLeastOne = true
val addressList = event.deleteAddressTags()
val addressSet = addressList.toSet()
addressList
.mapNotNull { getAddressableNoteIfExists(it) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) <= event.createdAt) {
// Counts the replies
deleteNote(deleteNote)
addressables.remove(deleteNote.idHex)
deletedAtLeastOne = true
}
}
notes.forEach { key, note ->
val noteEvent = note.event
if (noteEvent is AddressableEvent && noteEvent.addressTag() in addressSet) {
if (noteEvent.pubKey() == event.pubKey && noteEvent.createdAt() <= event.createdAt) {
deleteNote(note)
deletedAtLeastOne = true
}
}
}
val addressList = event.deleteAddresses()
val addressSet = addressList.toSet()
addressList
.mapNotNull { getAddressableNoteIfExists(it.toTag()) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) < event.createdAt) {
// Counts the replies
deleteNote(deleteNote)
addressables.remove(deleteNote.idHex)
deletedAtLeastOne = true
}
if (deletedAtLeastOne) {
val note = Note(event.id)
note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList())
refreshObservers(note)
}
notes.forEach { key, note ->
val noteEvent = note.event
if (noteEvent is AddressableEvent && noteEvent.address() in addressSet) {
if (noteEvent.pubKey() == event.pubKey && noteEvent.createdAt() <= event.createdAt) {
deleteNote(note)
deletedAtLeastOne = true
}
}
}
if (deletedAtLeastOne) {
val note = Note(event.id)
note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList())
refreshObservers(note)
}
}
@ -1591,7 +1607,7 @@ object LocalCache {
refreshObservers(note)
}
private fun consume(
fun consume(
event: SealedGossipEvent,
relay: Relay?,
) {
@ -2098,7 +2114,7 @@ object LocalCache {
}
}
private fun consume(
fun consume(
event: DraftEvent,
relay: Relay?,
) {
@ -2314,7 +2330,14 @@ object LocalCache {
}
fun hasConsumed(notificationEvent: Event): Boolean {
return notes.containsKey(notificationEvent.id)
return if (notificationEvent is AddressableEvent) {
val note = addressables.get(notificationEvent.addressTag())
val noteEvent = note?.event
noteEvent != null && notificationEvent.createdAt <= noteEvent.createdAt()
} else {
val note = notes.get(notificationEvent.id)
note?.event != null
}
}
}

Wyświetl plik

@ -39,6 +39,7 @@ object HttpClientManager {
var proxyChangeListeners = ArrayList<() -> Unit>()
private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI
private var defaultHttpClient: OkHttpClient? = null
private var defaultHttpClientWithoutProxy: OkHttpClient? = null
// fires off every time value of the property changes
private var internalProxy: Proxy? by
@ -58,6 +59,10 @@ object HttpClientManager {
}
}
fun getDefaultProxy(): Proxy? {
return this.internalProxy
}
fun setDefaultTimeout(timeout: Duration) {
Log.d("HttpClient", "Changing timeout to: $timeout")
if (this.defaultTimeout.seconds != timeout.seconds) {
@ -98,11 +103,18 @@ object HttpClientManager {
}
}
fun getHttpClient(): OkHttpClient {
if (this.defaultHttpClient == null) {
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
fun getHttpClient(useProxy: Boolean = true): OkHttpClient {
return if (useProxy) {
if (this.defaultHttpClient == null) {
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
}
defaultHttpClient!!
} else {
if (this.defaultHttpClientWithoutProxy == null) {
this.defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
}
defaultHttpClientWithoutProxy!!
}
return defaultHttpClient!!
}
fun initProxy(

Wyświetl plik

@ -121,8 +121,9 @@ class Nip11Retriever {
try {
val request: Request =
Request.Builder().header("Accept", "application/nostr+json").url(url).build()
val isLocalHost = dirtyUrl.startsWith("ws://127.0.0.1") || dirtyUrl.startsWith("ws://localhost")
HttpClientManager.getHttpClient()
HttpClientManager.getHttpClient(useProxy = !isLocalHost)
.newCall(request)
.enqueue(
object : Callback {

Wyświetl plik

@ -268,42 +268,69 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
// Avoid decrypting over and over again if the event already exist.
if (!event.isDeleted()) {
val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return
val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
val noteEvent = note?.event
if (noteEvent != null) {
if (event.createdAt > noteEvent.createdAt() || relay.brief !in note.relays) {
LocalCache.consume(event, relay)
}
} else {
// decrypts
event.cachedDraft(account.signer) {}
// decrypts
event.cachedDraft(account.signer) {}
LocalCache.justConsume(event, relay)
LocalCache.justConsume(event, relay)
}
}
}
is GiftWrapEvent -> {
// Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return
event.cachedGift(account.signer) { this.consume(it, relay) }
val noteEvent = note?.event as? GiftWrapEvent
if (noteEvent != null) {
if (relay.brief !in note.relays) {
LocalCache.justConsume(noteEvent, relay)
noteEvent.cachedGift(account.signer) {
this.consume(it, relay)
}
}
} else {
// new event
event.cachedGift(account.signer) { this.consume(it, relay) }
LocalCache.justConsume(event, relay)
}
}
is SealedGossipEvent -> {
// Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
val noteEvent = note?.event as? SealedGossipEvent
if (noteEvent != null) {
if (relay.brief !in note.relays) {
// adds the relay to seal and inner chat
LocalCache.consume(noteEvent, relay)
noteEvent.cachedGossip(account.signer) {
LocalCache.justConsume(it, relay)
}
}
} else {
// new event
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
LocalCache.justConsume(event, relay)
}
}
is LnZapEvent -> {
// Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return
event.zapRequest?.let {
if (it.isPrivateZap()) {
it.decryptPrivateZap(account.signer) {}
if (note?.event == null) {
event.zapRequest?.let {
if (it.isPrivateZap()) {
it.decryptPrivateZap(account.signer) {}
}
}
LocalCache.justConsume(event, relay)
}
}

Wyświetl plik

@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
@ -63,7 +63,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
// downloads linked events to this event.
return TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(ChannelCreateEvent.KIND),
@ -86,7 +86,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
return directEventsToLoad.map {
it.address().let { aTag ->
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(aTag.kind),

Wyświetl plik

@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
@ -60,7 +60,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return groupByEOSEPresence(addressesToWatch).map {
listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds =
@ -82,7 +82,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
),
),
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds =
@ -110,7 +110,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
it.address()?.let { aTag ->
if (aTag.kind < 25000 && aTag.dTag.isBlank()) {
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(aTag.kind),
@ -120,7 +120,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
)
} else {
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(aTag.kind),
@ -142,7 +142,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return groupByEOSEPresence(eventsToWatch).map {
listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds =
@ -165,7 +165,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
),
),
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds =
@ -190,9 +190,10 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return groupByEOSEPresence(eventsToWatch).map {
listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(TextNoteEvent.KIND),
tags = mapOf("q" to it.map { it.idHex }),
since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event.
@ -221,7 +222,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
// downloads linked events to this event.
return listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
ids = interestedEvents.toList(),

Wyświetl plik

@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EOSETime
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.MetadataEvent
@ -64,7 +65,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
val minEOSEs = findMinimumEOSEsForUsers(group)
listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND),
@ -73,7 +74,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
),
),
TypedFilter(
types = COMMON_FEED_TYPES,
types = EVENT_FINDER_TYPES,
filter =
JsonFilter(
kinds = listOf(ReportEvent.KIND),

Wyświetl plik

@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.service.HttpClientManager
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Lud06
import com.vitorpamplona.quartz.encoders.toLnUrl
import okhttp3.Request
import java.math.BigDecimal
import java.math.RoundingMode
@ -151,20 +150,6 @@ class LightningAddressResolver() {
}
}
fun lnAddressToLnUrl(
lnaddress: String,
onSuccess: (String) -> Unit,
onError: (String, String) -> Unit,
context: Context,
) {
fetchLightningAddressJson(
lnaddress,
onSuccess = { onSuccess(it.toByteArray().toLnUrl()) },
onError = onError,
context = context,
)
}
fun lnAddressInvoice(
lnaddress: String,
milliSats: Long,
@ -190,7 +175,8 @@ class LightningAddressResolver() {
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup,
R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user,
lnaddress,
),
)
null
@ -202,7 +188,8 @@ class LightningAddressResolver() {
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration,
R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user,
lnaddress,
),
)
}
@ -227,7 +214,8 @@ class LightningAddressResolver() {
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string
.error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup,
.error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user,
lnaddress,
),
)
null
@ -268,7 +256,8 @@ class LightningAddressResolver() {
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string
.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error,
.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user,
lnaddress,
reason,
),
)
@ -279,7 +268,8 @@ class LightningAddressResolver() {
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string
.unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json,
.unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user,
lnaddress,
),
)
}

Wyświetl plik

@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.notifications
import android.app.NotificationManager
import android.content.Context
import android.util.Log
import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R
@ -45,6 +46,7 @@ import java.math.BigDecimal
class EventNotificationConsumer(private val applicationContext: Context) {
suspend fun consume(event: GiftWrapEvent) {
Log.d("EventNotificationConsumer", "New Notification Arrived")
if (!LocalCache.justVerify(event)) return
if (!notificationManager().areNotificationsEnabled()) return
@ -64,14 +66,23 @@ class EventNotificationConsumer(private val applicationContext: Context) {
account: Account,
) {
pushWrappedEvent.cachedGift(account.signer) { notificationEvent ->
if (!LocalCache.hasConsumed(notificationEvent) && LocalCache.justVerify(notificationEvent)) {
val consumed = LocalCache.hasConsumed(notificationEvent)
val verified = LocalCache.justVerify(notificationEvent)
Log.d("EventNotificationConsumer", "New Notification Arrived for ${account.userProfile().toBestDisplayName()} consumed= $consumed && verified= $verified")
if (!consumed && verified) {
Log.d("EventNotificationConsumer", "New Notification was verified")
unwrapAndConsume(notificationEvent, account) { innerEvent ->
if (!LocalCache.hasConsumed(innerEvent)) {
Log.d("EventNotificationConsumer", "Unwrapped consume $consumed ${innerEvent.javaClass.simpleName}")
if (!consumed) {
if (innerEvent is PrivateDmEvent) {
Log.d("EventNotificationConsumer", "New Nip-04 DM to Notify")
notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) {
Log.d("EventNotificationConsumer", "New Zap to Notify")
notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) {
Log.d("EventNotificationConsumer", "New ChatMessage to Notify")
notify(innerEvent, account)
}
}
@ -86,6 +97,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
onReady: (Event) -> Unit,
) {
if (!LocalCache.justVerify(event)) return
if (LocalCache.hasConsumed(event)) return
when (event) {
is GiftWrapEvent -> {
@ -93,9 +105,11 @@ class EventNotificationConsumer(private val applicationContext: Context) {
}
is SealedGossipEvent -> {
event.cachedGossip(account.signer) {
// this is not verifiable
LocalCache.justConsume(it, null)
onReady(it)
if (!LocalCache.hasConsumed(it)) {
// this is not verifiable
LocalCache.justConsume(it, null)
onReady(it)
}
}
}
else -> {

Wyświetl plik

@ -50,6 +50,9 @@ enum class FeedType {
val COMMON_FEED_TYPES =
setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
val EVENT_FINDER_TYPES =
setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL)
class Relay(
val url: String,
val read: Boolean = true,
@ -63,7 +66,12 @@ class Relay(
const val RECONNECTING_IN_SECONDS = 60 * 3
}
private val httpClient = HttpClientManager.getHttpClient()
private val httpClient =
if (url.startsWith("ws://127.0.0.1") || url.startsWith("ws://localhost")) {
HttpClientManager.getHttpClient(false)
} else {
HttpClientManager.getHttpClient()
}
private var listeners = setOf<Listener>()
private var socket: WebSocket? = null

Wyświetl plik

@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.actions
import com.vitorpamplona.amethyst.service.HttpClientManager
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import java.net.HttpURLConnection
@ -36,7 +37,12 @@ class ImageDownloader {
try {
HttpURLConnection.setFollowRedirects(true)
var url = URL(imageUrl)
var huc = url.openConnection() as HttpURLConnection
var huc =
if (HttpClientManager.getDefaultProxy() != null) {
url.openConnection(HttpClientManager.getDefaultProxy()) as HttpURLConnection
} else {
url.openConnection() as HttpURLConnection
}
huc.instanceFollowRedirects = true
var responseCode = huc.responseCode
@ -45,7 +51,12 @@ class ImageDownloader {
// open the new connnection again
url = URL(newUrl)
huc = url.openConnection() as HttpURLConnection
huc =
if (HttpClientManager.getDefaultProxy() != null) {
url.openConnection(HttpClientManager.getDefaultProxy()) as HttpURLConnection
} else {
url.openConnection() as HttpURLConnection
}
responseCode = huc.responseCode
}

Wyświetl plik

@ -146,9 +146,9 @@ class NewMessageTagger(
fun getNostrAddress(
bechAddress: String,
restOfTheWord: String,
restOfTheWord: String?,
): String {
return if (restOfTheWord.isEmpty()) {
return if (restOfTheWord.isNullOrEmpty()) {
"nostr:$bechAddress"
} else {
if (Bech32.ALPHABET.contains(restOfTheWord.get(0), true)) {
@ -159,7 +159,7 @@ class NewMessageTagger(
}
}
@Immutable data class DirtyKeyInfo(val key: Nip19Bech32.ParseReturn, val restOfWord: String)
@Immutable data class DirtyKeyInfo(val key: Nip19Bech32.ParseReturn, val restOfWord: String?)
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
var key = mightBeAKey
@ -181,7 +181,7 @@ class NewMessageTagger(
val pubkey =
Nip19Bech32.uriToRoute(KeyPair(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null })
} else if (key.startsWith("npub1", true)) {
if (key.length < 63) {
return null
@ -192,7 +192,7 @@ class NewMessageTagger(
val pubkey = Nip19Bech32.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null })
} else if (key.startsWith("note1", true)) {
if (key.length < 63) {
return null
@ -203,7 +203,7 @@ class NewMessageTagger(
val noteId = Nip19Bech32.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(noteId, restOfWord)
return DirtyKeyInfo(noteId, restOfWord.ifEmpty { null })
} else if (key.startsWith("nprofile", true)) {
val pubkeyRelay = Nip19Bech32.uriToRoute(key) ?: return null

Wyświetl plik

@ -119,7 +119,7 @@ fun LoadOrCreateNote(
@Composable
private fun LoadAndDisplayEvent(
event: Event,
additionalChars: String,
additionalChars: String?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@ -141,7 +141,7 @@ private fun LoadAndDisplayEvent(
private fun DisplayEvent(
hex: HexKey,
kind: Int?,
additionalChars: String,
additionalChars: String?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@ -164,7 +164,7 @@ private fun DisplayNoteLink(
it: Note,
hex: HexKey,
kind: Int?,
addedCharts: String,
addedCharts: String?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@ -218,7 +218,7 @@ private fun DisplayNoteLink(
@Composable
private fun DisplayAddress(
nip19: Nip19Bech32.NAddress,
additionalChars: String,
additionalChars: String?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@ -245,16 +245,22 @@ private fun DisplayAddress(
}
if (noteBase == null) {
Text(
remember { "@${nip19.atag}$additionalChars" },
)
if (additionalChars != null) {
Text(
remember { "@${nip19.atag}$additionalChars" },
)
} else {
Text(
remember { "@${nip19.atag}" },
)
}
}
}
@Composable
private fun DisplayUser(
public fun DisplayUser(
userHex: HexKey,
additionalChars: String,
additionalChars: String?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@ -274,30 +280,34 @@ private fun DisplayUser(
userBase?.let { RenderUserAsClickableText(it, additionalChars, nav) }
if (userBase == null) {
Text(
remember { "@${userHex}$additionalChars" },
)
if (additionalChars != null) {
Text(
remember { "@${userHex}$additionalChars" },
)
} else {
Text(
remember { "@$userHex" },
)
}
}
}
@Composable
private fun RenderUserAsClickableText(
baseUser: User,
additionalChars: String,
additionalChars: String?,
nav: (String) -> Unit,
) {
val userState by baseUser.live().userMetadataInfo.observeAsState()
userState?.bestName()?.let {
CreateClickableTextWithEmoji(
clickablePart = it,
suffix = additionalChars.ifBlank { null },
maxLines = 1,
route = "User/${baseUser.pubkeyHex}",
nav = nav,
tags = userState?.tags ?: EmptyTagList,
)
}
CreateClickableTextWithEmoji(
clickablePart = userState?.bestName() ?: ("@" + baseUser.pubkeyDisplayHex()),
suffix = additionalChars?.ifBlank { null },
maxLines = 1,
route = "User/${baseUser.pubkeyHex}",
nav = nav,
tags = userState?.tags ?: EmptyTagList,
)
}
@Composable

Wyświetl plik

@ -68,15 +68,16 @@ fun ExpandableRichTextViewer(
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
var showFullText by remember {
val cached = ShowFullTextCache.cache[id]
if (cached == null) {
ShowFullTextCache.cache.put(id, false)
mutableStateOf(false)
} else {
mutableStateOf(cached)
var showFullText by
remember {
val cached = ShowFullTextCache.cache[id]
if (cached == null) {
ShowFullTextCache.cache.put(id, false)
mutableStateOf(false)
} else {
mutableStateOf(cached)
}
}
}
val whereToCut = remember(content) { ExpandableTextCutOffCalculator.indexToCutOff(content) }

Wyświetl plik

@ -61,25 +61,7 @@ fun LoadUrlPreview(
) { state ->
when (state) {
is UrlPreviewState.Loaded -> {
if (state.previewInfo.mimeType.type == "image") {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlImage(url),
roundedCorner = true,
accountViewModel = accountViewModel,
)
}
} else if (state.previewInfo.mimeType.type == "video") {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlVideo(url),
roundedCorner = true,
accountViewModel = accountViewModel,
)
}
} else {
UrlPreviewCard(url, state.previewInfo)
}
RenderLoaded(state, url, accountViewModel)
}
else -> {
ClickableUrl(urlText, url)
@ -88,3 +70,30 @@ fun LoadUrlPreview(
}
}
}
@Composable
fun RenderLoaded(
state: UrlPreviewState.Loaded,
url: String,
accountViewModel: AccountViewModel,
) {
if (state.previewInfo.mimeType.type == "image") {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlImage(url),
roundedCorner = true,
accountViewModel = accountViewModel,
)
}
} else if (state.previewInfo.mimeType.type == "video") {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlVideo(url),
roundedCorner = true,
accountViewModel = accountViewModel,
)
}
} else {
UrlPreviewCard(url, state.previewInfo)
}
}

Wyświetl plik

@ -1,209 +0,0 @@
/**
* 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.util.Log
import android.util.Patterns
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import kotlinx.coroutines.CancellationException
class MarkdownParser {
private fun getDisplayNameAndNIP19FromTag(
tag: String,
tags: ImmutableListOfLists<String>,
): Pair<String, String>? {
val matcher = RichTextParser.tagIndex.matcher(tag)
val (index, suffix) =
try {
matcher.find()
Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "")
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.w("Tag Parser", "Couldn't link tag $tag", e)
Pair(null, null)
}
if (index != null && index >= 0 && index < tags.lists.size) {
val tag = tags.lists[index]
if (tag.size > 1) {
if (tag[0] == "p") {
LocalCache.checkGetOrCreateUser(tag[1])?.let {
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
}
} else if (tag[0] == "e" || tag[0] == "a") {
LocalCache.checkGetOrCreateNote(tag[1])?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
}
}
}
return null
}
private suspend fun getDisplayNameFromNip19(nip19: Nip19Bech32.Entity): Pair<String, String>? {
return when (nip19) {
is Nip19Bech32.NSec -> null
is Nip19Bech32.NPub -> {
LocalCache.getUserIfExists(nip19.hex)?.let {
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
}
}
is Nip19Bech32.NProfile -> {
LocalCache.getUserIfExists(nip19.hex)?.let {
return Pair(it.toBestDisplayName(), it.pubkeyNpub())
}
}
is Nip19Bech32.Note -> {
LocalCache.getNoteIfExists(nip19.hex)?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
}
is Nip19Bech32.NEvent -> {
LocalCache.getNoteIfExists(nip19.hex)?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
}
is Nip19Bech32.NEmbed -> {
if (LocalCache.getNoteIfExists(nip19.event.id) == null) {
LocalCache.verifyAndConsume(nip19.event, null)
}
LocalCache.getNoteIfExists(nip19.event.id)?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
}
is Nip19Bech32.NRelay -> null
is Nip19Bech32.NAddress -> {
LocalCache.getAddressableNoteIfExists(nip19.atag)?.let {
return Pair(it.idDisplayNote(), it.toNEvent())
}
}
else -> null
}
}
fun returnNIP19References(
content: String,
tags: ImmutableListOfLists<String>?,
): List<Nip19Bech32.Entity> {
checkNotInMainThread()
val listOfReferences = mutableListOf<Nip19Bech32.Entity>()
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
if (RichTextParser.startsWithNIP19Scheme(word)) {
val parsedNip19 = Nip19Bech32.uriToRoute(word)
parsedNip19?.let { listOfReferences.add(it.entity) }
}
}
}
tags?.lists?.forEach {
if (it[0] == "p" && it.size > 1) {
listOfReferences.add(Nip19Bech32.NProfile(it[1], listOfNotNull(it.getOrNull(2))))
} else if (it[0] == "e" && it.size > 1) {
listOfReferences.add(Nip19Bech32.NEvent(it[1], listOfNotNull(it.getOrNull(2)), null, null))
} else if (it[0] == "a" && it.size > 1) {
ATag.parseAtag(it[1], it.getOrNull(2))?.let { atag ->
listOfReferences.add(Nip19Bech32.NAddress(it[1], listOfNotNull(atag.relay), atag.pubKeyHex, atag.kind))
}
}
}
return listOfReferences
}
suspend fun returnMarkdownWithSpecialContent(
content: String,
tags: ImmutableListOfLists<String>?,
): String {
var returnContent = ""
content.split('\n').forEach { paragraph ->
paragraph.split(' ').forEach { word: String ->
if (RichTextParser.isValidURL(word)) {
if (RichTextParser.isImageUrl(word)) {
returnContent += "![]($word) "
} else {
returnContent += "[$word]($word) "
}
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
returnContent += "[$word](mailto:$word) "
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
returnContent += "[$word](tel:$word) "
} else if (RichTextParser.startsWithNIP19Scheme(word)) {
val parsedNip19 = Nip19Bech32.uriToRoute(word)
returnContent +=
if (parsedNip19?.entity !== null) {
val pair = getDisplayNameFromNip19(parsedNip19.entity)
if (pair != null) {
val (displayName, nip19) = pair
"[$displayName](nostr:$nip19) "
} else {
"$word "
}
} else {
"$word "
}
} else if (word.startsWith("#")) {
if (RichTextParser.tagIndex.matcher(word).matches() && tags != null) {
val pair = getDisplayNameAndNIP19FromTag(word, tags)
if (pair != null) {
returnContent += "[${pair.first}](nostr:${pair.second}) "
} else {
returnContent += "$word "
}
} else if (RichTextParser.hashTagsPattern.matcher(word).matches()) {
val hashtagMatcher = RichTextParser.hashTagsPattern.matcher(word)
val (myTag, mySuffix) =
try {
hashtagMatcher.find()
Pair(hashtagMatcher.group(1), hashtagMatcher.group(2))
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("Hashtag Parser", "Couldn't link hashtag $word", e)
Pair(null, null)
}
if (myTag != null) {
returnContent += "[#$myTag](nostr:Hashtag?id=$myTag)$mySuffix "
} else {
returnContent += "$word "
}
} else {
returnContent += "$word "
}
} else {
returnContent += "$word "
}
}
returnContent += "\n"
}
return returnContent
}
}

Wyświetl plik

@ -29,12 +29,12 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@ -51,9 +51,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
@ -65,9 +62,6 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.lifecycle.viewmodel.compose.viewModel
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.markdown.MarkdownParseOptions
import com.halilibo.richtext.ui.material3.Material3RichText
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
import com.vitorpamplona.amethyst.commons.richtext.BechSegment
import com.vitorpamplona.amethyst.commons.richtext.CashuSegment
@ -79,10 +73,8 @@ import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
import com.vitorpamplona.amethyst.commons.richtext.ImageSegment
import com.vitorpamplona.amethyst.commons.richtext.InvoiceSegment
import com.vitorpamplona.amethyst.commons.richtext.LinkSegment
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.PhoneSegment
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment
import com.vitorpamplona.amethyst.commons.richtext.Segment
@ -93,33 +85,29 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.service.CachedRichTextParser
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
import com.vitorpamplona.amethyst.ui.note.LoadUser
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Font17SP
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
import com.vitorpamplona.amethyst.ui.theme.inlinePlaceholder
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
import com.vitorpamplona.amethyst.ui.uriToRoute
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
fun isMarkdown(content: String): Boolean {
return content.startsWith("> ") ||
content.startsWith("# ") ||
content.contains("##") ||
content.contains("__") ||
content.contains("**") ||
content.contains("```") ||
content.contains("](")
}
@ -137,7 +125,7 @@ fun RichTextViewer(
) {
Column(modifier = modifier) {
if (remember(content) { isMarkdown(content) }) {
RenderContentAsMarkdown(content, tags, accountViewModel, nav)
RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
} else {
RenderRegular(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
}
@ -346,17 +334,6 @@ fun RenderRegular(
}
}
}
/*
// UrlPreviews and Images have a 5dp spacing down. This also adds the space to Text.
val lastElement = state.paragraphs.lastOrNull()?.words?.lastOrNull()
if (lastElement !is ImageSegment &&
lastElement !is LinkSegment &&
lastElement !is InvoiceSegment &&
lastElement !is CashuSegment
) {
Spacer(modifier = StdVertSpacer)
}*/
}
}
@ -462,186 +439,6 @@ fun RenderCustomEmoji(
)
}
val markdownParseOptions =
MarkdownParseOptions(
autolink = true,
isImage = { url -> RichTextParser.isImageOrVideoUrl(url) },
)
@Composable
private fun RenderContentAsMarkdown(
content: String,
tags: ImmutableListOfLists<String>?,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val uri = LocalUriHandler.current
val onClick =
remember {
{ link: String ->
val route = uriToRoute(link)
if (route != null) {
nav(route)
} else {
runCatching { uri.openUri(link) }
}
Unit
}
}
ProvideTextStyle(MarkdownTextStyle) {
Material3RichText(style = MaterialTheme.colorScheme.markdownStyle) {
RefreshableContent(content, tags, accountViewModel) {
Markdown(
content = it,
markdownParseOptions = markdownParseOptions,
onLinkClicked = onClick,
onMediaCompose = { title, destination ->
ZoomableContentView(
content =
remember(destination, tags) {
RichTextParser().parseMediaUrl(
destination,
tags ?: EmptyTagList,
title.ifEmpty { null } ?: content,
) ?: MediaUrlImage(url = destination, description = title.ifEmpty { null } ?: content)
},
roundedCorner = true,
accountViewModel = accountViewModel,
)
},
)
}
}
}
}
@Composable
private fun RefreshableContent(
content: String,
tags: ImmutableListOfLists<String>?,
accountViewModel: AccountViewModel,
onCompose: @Composable (String) -> Unit,
) {
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(content) }
ObserverAllNIP19References(content, tags, accountViewModel) {
accountViewModel.returnMarkdownWithSpecialContent(content, tags) { newMarkdownWithSpecialContent ->
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
markdownWithSpecialContent = newMarkdownWithSpecialContent
}
}
}
markdownWithSpecialContent?.let { onCompose(it) }
}
@Composable
fun ObserverAllNIP19References(
content: String,
tags: ImmutableListOfLists<String>?,
accountViewModel: AccountViewModel,
onRefresh: () -> Unit,
) {
var nip19References by remember(content) { mutableStateOf<List<Nip19Bech32.Entity>>(emptyList()) }
LaunchedEffect(key1 = content) {
accountViewModel.returnNIP19References(content, tags) {
nip19References = it
onRefresh()
}
}
nip19References.forEach { ObserveNIP19(it, accountViewModel, onRefresh) }
}
@Composable
fun ObserveNIP19(
entity: Nip19Bech32.Entity,
accountViewModel: AccountViewModel,
onRefresh: () -> Unit,
) {
when (entity) {
is Nip19Bech32.NPub -> ObserveNIP19User(entity.hex, accountViewModel, onRefresh)
is Nip19Bech32.NProfile -> ObserveNIP19User(entity.hex, accountViewModel, onRefresh)
is Nip19Bech32.Note -> ObserveNIP19Event(entity.hex, accountViewModel, onRefresh)
is Nip19Bech32.NEvent -> ObserveNIP19Event(entity.hex, accountViewModel, onRefresh)
is Nip19Bech32.NEmbed -> ObserveNIP19Event(entity.event.id, accountViewModel, onRefresh)
is Nip19Bech32.NAddress -> ObserveNIP19Event(entity.atag, accountViewModel, onRefresh)
is Nip19Bech32.NSec -> {}
is Nip19Bech32.NRelay -> {}
}
}
@Composable
private fun ObserveNIP19Event(
hex: HexKey,
accountViewModel: AccountViewModel,
onRefresh: () -> Unit,
) {
var baseNote by remember(hex) { mutableStateOf<Note?>(accountViewModel.getNoteIfExists(hex)) }
if (baseNote == null) {
LaunchedEffect(key1 = hex) {
accountViewModel.checkGetOrCreateNote(hex) { note ->
launch(Dispatchers.Main) { baseNote = note }
}
}
}
baseNote?.let { note -> ObserveNote(note, onRefresh) }
}
@Composable
fun ObserveNote(
note: Note,
onRefresh: () -> Unit,
) {
val loadedNoteId by note.live().metadata.observeAsState()
LaunchedEffect(key1 = loadedNoteId) {
if (loadedNoteId != null) {
onRefresh()
}
}
}
@Composable
private fun ObserveNIP19User(
hex: HexKey,
accountViewModel: AccountViewModel,
onRefresh: () -> Unit,
) {
var baseUser by remember(hex) { mutableStateOf<User?>(accountViewModel.getUserIfExists(hex)) }
if (baseUser == null) {
LaunchedEffect(key1 = hex) {
accountViewModel.checkGetOrCreateUser(hex)?.let { user ->
launch(Dispatchers.Main) { baseUser = user }
}
}
}
baseUser?.let { user -> ObserveUser(user, onRefresh) }
}
@Composable
private fun ObserveUser(
user: User,
onRefresh: () -> Unit,
) {
val loadedUserMetaId by user.live().metadata.observeAsState()
LaunchedEffect(key1 = loadedUserMetaId) {
if (loadedUserMetaId != null) {
onRefresh()
}
}
}
@Composable
fun BechLink(
word: String,
@ -683,7 +480,7 @@ fun BechLink(
}
@Composable
private fun DisplayFullNote(
fun DisplayFullNote(
note: Note,
extraChars: String?,
quotesLeft: Int,
@ -752,13 +549,7 @@ fun HashTag(
@Composable
private fun InlineIcon(hashtagIcon: HashtagIcon) =
InlineTextContent(
Placeholder(
width = Font17SP,
height = Font17SP,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
),
) {
InlineTextContent(inlinePlaceholder) {
Icon(
imageVector = hashtagIcon.icon,
contentDescription = hashtagIcon.description,

Wyświetl plik

@ -141,6 +141,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable
@ -542,7 +543,10 @@ fun ShowHash(
if (content.hash != null) {
LaunchedEffect(key1 = content.url) {
val newVerifiedHash = verifyHash(content)
val newVerifiedHash =
withContext(Dispatchers.IO) {
verifyHash(content)
}
if (newVerifiedHash != verifiedHash) {
verifiedHash = newVerifiedHash
}

Wyświetl plik

@ -0,0 +1,307 @@
/**
* 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.markdown
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.halilibo.richtext.ui.MediaRenderer
import com.halilibo.richtext.ui.string.InlineContent
import com.halilibo.richtext.ui.string.RichTextString
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.HashtagIcon
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.ui.components.DisplayFullNote
import com.vitorpamplona.amethyst.ui.components.DisplayUser
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink
import com.vitorpamplona.amethyst.ui.theme.Font17SP
import com.vitorpamplona.amethyst.ui.theme.Size17Modifier
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import kotlinx.coroutines.runBlocking
class MarkdownMediaRenderer(
val startOfText: String,
val tags: ImmutableListOfLists<String>?,
val canPreview: Boolean,
val quotesLeft: Int,
val backgroundColor: MutableState<Color>,
val accountViewModel: AccountViewModel,
val nav: (String) -> Unit,
) : MediaRenderer {
val parser = RichTextParser()
override fun shouldRenderLinkPreview(
title: String?,
uri: String,
): Boolean {
return if (canPreview && uri.startsWith("http")) {
if (title.isNullOrBlank() || title == uri) {
true
} else {
false
}
} else {
false
}
}
override fun renderImage(
title: String?,
uri: String,
richTextStringBuilder: RichTextString.Builder,
) {
if (canPreview) {
val content =
parser.parseMediaUrl(
fullUrl = uri,
eventTags = tags ?: EmptyTagList,
description = title?.ifEmpty { null } ?: startOfText,
) ?: MediaUrlImage(url = uri, description = title?.ifEmpty { null } ?: startOfText)
renderInlineFullWidth(richTextStringBuilder) {
ZoomableContentView(
content = content,
roundedCorner = true,
accountViewModel = accountViewModel,
)
}
} else {
renderAsCompleteLink(title ?: uri, uri, richTextStringBuilder)
}
}
override fun renderLinkPreview(
title: String?,
uri: String,
richTextStringBuilder: RichTextString.Builder,
) {
val content = parser.parseMediaUrl(uri, eventTags = tags ?: EmptyTagList, startOfText)
if (canPreview) {
if (content != null) {
renderInlineFullWidth(richTextStringBuilder) {
ZoomableContentView(
content = content,
roundedCorner = true,
accountViewModel = accountViewModel,
)
}
} else {
if (!accountViewModel.settings.showUrlPreview.value) {
renderAsCompleteLink(title ?: uri, uri, richTextStringBuilder)
} else {
renderInlineFullWidth(richTextStringBuilder) {
LoadUrlPreview(uri, title ?: uri, accountViewModel)
}
}
}
} else {
renderAsCompleteLink(title ?: uri, uri, richTextStringBuilder)
}
}
override fun renderNostrUri(
uri: String,
richTextStringBuilder: RichTextString.Builder,
) {
// This should be fast, so it is ok.
val loadedLink =
accountViewModel.bechLinkCache.cached(uri)
?: runBlocking {
accountViewModel.bechLinkCache.update(uri)
}
val baseNote = loadedLink?.baseNote
if (canPreview && quotesLeft > 0 && baseNote != null) {
renderInlineFullWidth(richTextStringBuilder) {
Row {
DisplayFullNote(
note = baseNote,
extraChars = loadedLink.nip19.additionalChars?.ifBlank { null },
quotesLeft = quotesLeft,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
} else if (loadedLink?.nip19 != null) {
when (val entity = loadedLink.nip19.entity) {
is Nip19Bech32.NPub -> renderObservableUser(entity.hex, richTextStringBuilder)
is Nip19Bech32.NProfile -> renderObservableUser(entity.hex, richTextStringBuilder)
is Nip19Bech32.Note -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder)
is Nip19Bech32.NEvent -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder)
is Nip19Bech32.NEmbed -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder)
is Nip19Bech32.NAddress -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder)
is Nip19Bech32.NRelay -> renderShortNostrURI(uri, richTextStringBuilder)
is Nip19Bech32.NSec -> renderShortNostrURI(uri, richTextStringBuilder)
else -> renderShortNostrURI(uri, richTextStringBuilder)
}
} else {
renderShortNostrURI(uri, richTextStringBuilder)
}
}
override fun renderHashtag(
tag: String,
richTextStringBuilder: RichTextString.Builder,
) {
val tagWithoutHash = tag.removePrefix("#")
renderAsCompleteLink(tag, "nostr:Hashtag?id=$tagWithoutHash}", richTextStringBuilder)
val hashtagIcon: HashtagIcon? = checkForHashtagWithIcon(tagWithoutHash)
if (hashtagIcon != null) {
renderInline(richTextStringBuilder) {
Box(Size17Modifier) {
Icon(
imageVector = hashtagIcon.icon,
contentDescription = hashtagIcon.description,
tint = Color.Unspecified,
modifier = hashtagIcon.modifier,
)
}
}
}
}
fun renderObservableUser(
userHex: String,
richTextStringBuilder: RichTextString.Builder,
) {
renderInline(richTextStringBuilder) {
DisplayUser(userHex, null, accountViewModel, nav)
}
}
fun renderObservableShortNoteUri(
loadedLink: LoadedBechLink,
uri: String,
richTextStringBuilder: RichTextString.Builder,
) {
loadedLink.baseNote?.let { renderNoteObserver(it, richTextStringBuilder) }
renderShortNostrURI(uri, richTextStringBuilder)
}
private fun renderNoteObserver(
baseNote: Note,
richTextStringBuilder: RichTextString.Builder,
) {
renderInvisible(richTextStringBuilder) {
// Preloads note if not loaded yet.
baseNote.live().metadata.observeAsState()
}
}
private fun renderShortNostrURI(
uri: String,
richTextStringBuilder: RichTextString.Builder,
) {
val nip19 = "@" + uri.removePrefix("nostr:")
renderAsCompleteLink(
title =
if (nip19.length > 16) {
nip19.replaceRange(8, nip19.length - 8, ":")
} else {
nip19
},
destination = uri,
richTextStringBuilder = richTextStringBuilder,
)
}
private fun renderInvisible(
richTextStringBuilder: RichTextString.Builder,
innerComposable: @Composable () -> Unit,
) {
richTextStringBuilder.appendInlineContent(
content =
InlineContent(
initialSize = {
IntSize(0.dp.roundToPx(), 0.dp.roundToPx())
},
) {
innerComposable()
},
)
}
private fun renderInline(
richTextStringBuilder: RichTextString.Builder,
innerComposable: @Composable () -> Unit,
) {
richTextStringBuilder.appendInlineContent(
content =
InlineContent(
initialSize = {
IntSize(Font17SP.roundToPx(), Font17SP.roundToPx())
},
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
) {
innerComposable()
},
)
}
private fun renderInlineFullWidth(
richTextStringBuilder: RichTextString.Builder,
innerComposable: @Composable () -> Unit,
) {
richTextStringBuilder.appendInlineContentFullWidth(
content =
InlineContent(
initialSize = {
IntSize(Font17SP.roundToPx(), Font17SP.roundToPx())
},
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
) {
innerComposable()
},
)
}
private fun renderAsCompleteLink(
title: String,
destination: String,
richTextStringBuilder: RichTextString.Builder,
) {
richTextStringBuilder.pushFormat(
RichTextString.Format.Link(destination = destination),
)
richTextStringBuilder.append(title)
richTextStringBuilder.pop()
}
}

Wyświetl plik

@ -0,0 +1,91 @@
/**
* 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.markdown
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.commonmark.MarkdownParseOptions
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.ui.material3.RichText
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
import com.vitorpamplona.amethyst.ui.uriToRoute
import com.vitorpamplona.quartz.events.ImmutableListOfLists
@Composable
fun RenderContentAsMarkdown(
content: String,
tags: ImmutableListOfLists<String>?,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val uri = LocalUriHandler.current
val onClick =
remember {
{ link: String ->
val route = uriToRoute(link)
if (route != null) {
nav(route)
} else {
runCatching { uri.openUri(link) }
}
Unit
}
}
ProvideTextStyle(MarkdownTextStyle) {
val astNode =
remember(content) {
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
}
val renderer =
remember(content) {
MarkdownMediaRenderer(
content.take(100),
tags,
canPreview,
quotesLeft,
backgroundColor,
accountViewModel,
nav,
)
}
RichText(
style = MaterialTheme.colorScheme.markdownStyle,
linkClickHandler = onClick,
renderer = renderer,
) {
BasicMarkdown(astNode)
}
}
}

Wyświetl plik

@ -942,7 +942,7 @@ fun ZapReaction(
var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToSetCustomZap by remember { mutableStateOf(false) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
var showErrorMessageDialog by remember { mutableStateOf<List<String>>(emptyList()) }
var wantsToPay by
remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -972,7 +972,7 @@ fun ZapReaction(
onError = { _, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
showErrorMessageDialog = showErrorMessageDialog + message
}
},
onPayViaIntent = { wantsToPay = it },
@ -998,7 +998,7 @@ fun ZapReaction(
onError = { _, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
showErrorMessageDialog = showErrorMessageDialog + message
}
},
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
@ -1006,19 +1006,20 @@ fun ZapReaction(
)
}
if (showErrorMessageDialog != null) {
if (showErrorMessageDialog.isNotEmpty()) {
val msg = showErrorMessageDialog.joinToString("\n")
ErrorMessageDialog(
title = stringResource(id = R.string.error_dialog_zap_error),
textContent = showErrorMessageDialog ?: "",
textContent = msg,
onClickStartMessage = {
baseNote.author?.let {
scope.launch(Dispatchers.IO) {
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel)
val route = routeToMessage(it, msg, accountViewModel)
nav(route)
}
}
},
onDismiss = { showErrorMessageDialog = null },
onDismiss = { showErrorMessageDialog = emptyList() },
)
}
@ -1038,7 +1039,7 @@ fun ZapReaction(
wantsToPay = persistentListOf()
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
showErrorMessageDialog = showErrorMessageDialog + it
}
},
)
@ -1050,7 +1051,7 @@ fun ZapReaction(
onError = { _, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
showErrorMessageDialog = showErrorMessageDialog + message
}
},
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },

Wyświetl plik

@ -424,18 +424,17 @@ fun UpdateZapAmountDialog(
Modifier.weight(1f),
)
/* TODO: Find a way to open this in the PWA
IconButton(onClick = {
onClose()
runCatching { uri.openUri("https://app.mutinywallet.com/settings/connections?callbackUri=nostr+walletconnect&name=Amethyst") }
}) {
Icon(
painter = painterResource(R.mipmap.mutiny),
null,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified
)
}*/
IconButton(onClick = {
onClose()
runCatching { uri.openUri("https://app.mutinywallet.com/settings/connections?name=Amethyst") }
}) {
Icon(
painter = painterResource(R.mipmap.mutiny),
null,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified,
)
}
IconButton(
onClick = {

Wyświetl plik

@ -30,8 +30,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.AlertDialog
@ -361,7 +363,7 @@ fun PayViaIntentDialog(
),
) {
Surface {
Column(modifier = Modifier.padding(10.dp)) {
Column(modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState())) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,

Wyświetl plik

@ -363,7 +363,7 @@ fun NoteMaster(
reports,
note.author?.let { account.isHidden(it) } ?: false,
accountViewModel,
Modifier,
Modifier.fillMaxWidth(),
nav,
onClick = { showHiddenNote = true },
)

Wyświetl plik

@ -48,6 +48,7 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -82,9 +83,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.FragmentActivity
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.commonmark.MarkdownParseOptions
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.Material3RichText
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
@ -140,12 +143,18 @@ fun AccountBackupDialog(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Material3RichText(
val content1 = stringResource(R.string.account_backup_tips2_md)
val astNode1 =
remember {
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
}
RichText(
style = RichTextStyle().resolveDefaults(),
renderer = null,
) {
Markdown(
content = stringResource(R.string.account_backup_tips2_md),
)
BasicMarkdown(astNode1)
}
Spacer(modifier = Modifier.height(10.dp))
@ -154,12 +163,18 @@ fun AccountBackupDialog(
Spacer(modifier = Modifier.height(30.dp))
Material3RichText(
val content = stringResource(R.string.account_backup_tips3_md)
val astNode =
remember {
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
}
RichText(
style = RichTextStyle().resolveDefaults(),
renderer = null,
) {
Markdown(
content = stringResource(R.string.account_backup_tips3_md),
)
BasicMarkdown(astNode)
}
Spacer(modifier = Modifier.height(10.dp))
@ -312,7 +327,7 @@ private fun EncryptNSecCopyButton(
}
}
Button(
OutlinedButton(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
authenticate(
@ -324,15 +339,10 @@ private fun EncryptNSecCopyButton(
)
},
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
contentPadding = ButtonPadding,
enabled = password.value.text.isNotBlank(),
) {
Icon(
tint = MaterialTheme.colorScheme.onPrimary,
imageVector = Icons.Default.Key,
contentDescription =
stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup),
@ -340,7 +350,6 @@ private fun EncryptNSecCopyButton(
)
Text(
stringResource(id = R.string.encrypt_and_copy_my_secret_key),
color = MaterialTheme.colorScheme.onPrimary,
)
}
}

Wyświetl plik

@ -59,7 +59,6 @@ import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.actions.Dao
import com.vitorpamplona.amethyst.ui.components.BundledInsert
import com.vitorpamplona.amethyst.ui.components.MarkdownParser
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.navigation.bottomNavigationItems
@ -79,7 +78,6 @@ import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.Participant
@ -1015,33 +1013,6 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
}
}
fun returnNIP19References(
content: String,
tags: ImmutableListOfLists<String>?,
onNewReferences: (List<Nip19Bech32.Entity>) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
onNewReferences(MarkdownParser().returnNIP19References(content, tags))
}
}
fun returnMarkdownWithSpecialContent(
content: String,
tags: ImmutableListOfLists<String>?,
onNewContent: (String) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
onNewContent(MarkdownParser().returnMarkdownWithSpecialContent(content, tags))
}
}
fun loadNEmbedIfNeeded(nembed: Event) {
val baseNote = LocalCache.getNoteIfExists(nembed.id)
if (baseNote?.event == null) {
LocalCache.verifyAndConsume(nembed, null)
}
}
fun checkIsOnline(
media: String?,
onDone: (Boolean) -> Unit,
@ -1374,11 +1345,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
is Nip19Bech32.NEvent -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } }
is Nip19Bech32.NEmbed ->
withContext(Dispatchers.IO) {
accountViewModel.loadNEmbedIfNeeded(parsed.event)
LocalCache.checkGetOrCreateNote(parsed.event.id)?.let { note ->
returningNote = note
val baseNote = LocalCache.getOrCreateNote(parsed.event)
if (baseNote.event == null) {
launch(Dispatchers.IO) {
LocalCache.verifyAndConsume(parsed.event, null)
}
}
returningNote = baseNote
}
is Nip19Bech32.NRelay -> {}
is Nip19Bech32.NAddress -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.atag)?.let { note -> returningNote = note } }

Wyświetl plik

@ -36,6 +36,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -47,8 +48,10 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.material3.Material3RichText
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.commonmark.MarkdownParseOptions
import com.halilibo.richtext.markdown.BasicMarkdown
import com.halilibo.richtext.ui.material3.RichText
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
@ -112,12 +115,18 @@ fun ConnectOrbotDialog(
)
Row {
Material3RichText(
val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown)
val astNode1 =
remember {
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
}
RichText(
style = myMarkDownStyle,
renderer = null,
) {
Markdown(
content = stringResource(R.string.connect_through_your_orbot_setup_markdown),
)
BasicMarkdown(astNode1)
}
}

Wyświetl plik

@ -36,6 +36,8 @@ import androidx.compose.material3.Shapes
import androidx.compose.ui.Modifier
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.dp
val Shapes =
@ -231,3 +233,10 @@ val liveStreamTag =
val chatAuthorBox = Modifier.size(20.dp)
val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape)
val AuthorInfoVideoFeed = Modifier.width(75.dp).padding(end = 15.dp)
val inlinePlaceholder =
Placeholder(
width = Font17SP,
height = Font17SP,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
)

Wyświetl plik

@ -59,7 +59,7 @@ val Font18SP = 18.sp
val MarkdownTextStyle = TextStyle(lineHeight = 1.30.em)
val DefaultParagraphSpacing: TextUnit = 16.sp
val DefaultParagraphSpacing: TextUnit = 18.sp
internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->
when (level) {

Wyświetl plik

@ -276,6 +276,7 @@
<string name="report_dialog_title">Bloquear y reportar</string>
<string name="block_only">Bloquear</string>
<string name="bookmarks">Marcadores</string>
<string name="drafts">Borradores</string>
<string name="private_bookmarks">Marcadores privados</string>
<string name="public_bookmarks">Marcadores públicos</string>
<string name="add_to_private_bookmarks">Agregar a marcadores privados</string>

Wyświetl plik

@ -1,2 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"></resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="point_to_the_qr_code">क्यूआर क्रमचित्र के अभिमुख करें</string>
<string name="show_qr">क्यूआर क्रमचित्र दिखाएँ</string>
<string name="profile_image">स्व चित्र</string>
<string name="your_profile_image">आपका चित्र</string>
<string name="scan_qr">क्यूआर क्रमचित्र का जाँच करें</string>
<string name="show_anyway">फिर भी दिखाएँ</string>
<string name="post_was_hidden">इस लेख को छिपाया गया क्योंकि इसमें आपके आच्छाद्य उपयोगकर्ता अथवा शब्द उल्लेखित हैं</string>
<string name="post_was_flagged_as_inappropriate_by">लेख को शान्त किया गया अथवा आपत्ति उठाया गया इनके द्वारा</string>
<string name="post_not_found">घटना उपलब्ध किया जा रहा है अथवा आपके पुनःप्रसारक सूची द्वारा प्राप्य नही</string>
<string name="channel_image">प्रणाली चित्र</string>
<string name="referenced_event_not_found">उद्धृत घटना अप्राप्त</string>
<string name="could_not_decrypt_the_message">सन्देश का अरहस्यकरण असफल</string>
<string name="group_picture">समूह चित्र</string>
<string name="explicit_content">अभद्र विषयवस्तु</string>
<string name="spam">कचरा</string>
<string name="impersonation">ढोंग</string>
<string name="illegal_behavior">अवैध बरताव</string>
<string name="other">अन्य</string>
<string name="unknown">अज्ञात</string>
<string name="relay_icon">पनःप्रसारक चिह्न</string>
<string name="unknown_author">अज्ञात लेखक</string>
<string name="copy_text">लेख की प्रतिलिपि बनाएँ</string>
<string name="copy_user_pubkey">लेखक विभेदक का प्रतिलिपि करें</string>
<string name="copy_note_id">टीका विभेदक का प्रतिलिपि करें</string>
<string name="broadcast">प्रसारण</string>
<string name="timestamp_it">समयांकन करें</string>
<string name="timestamp_pending">समयांकन : शेष पुष्टिकरण</string>
<string name="timestamp_pending_short">समयांकन : शेष</string>
<string name="request_deletion">हटाने की याचना</string>
<string name="block_report">अवरोधित करें / सूचना दें</string>
<string name="block_hide_user"><![CDATA[अवरोधित करें तथा उपयोगकर्ता को छिपाएँ]]></string>
<string name="report_spam_scam">कचरा / घोटाला की सूचना दें</string>
<string name="report_impersonation">ढोंग की सूचना दें</string>
<string name="report_explicit_content">अभद्र विषयवस्तु की सूचना दें</string>
<string name="report_illegal_behaviour">अवैध बरताव की सूचना दें</string>
<string name="login_with_a_private_key_to_be_able_to_reply">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। प्रत्युत्तर देने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
<string name="login_with_a_private_key_to_be_able_to_boost_posts">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। टीकाओं को उद्धृत करने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
<string name="login_with_a_private_key_to_like_posts">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। टीकाओं को चाहने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
<string name="no_zap_amount_setup_long_press_to_change">ज्साप संख्या स्थापित नही किया गया। दीर्घतः दबाएँ परिवर्तित करने के लिए</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। ज्साप भेजने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
<string name="login_with_a_private_key_to_be_able_to_follow">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। अनुचरण करने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। अनुचरण रोकने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
<string name="login_with_a_private_key_to_be_able_to_hide_word">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। शब्द अथवा वाक्य छिपाने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
<string name="login_with_a_private_key_to_be_able_to_show_word">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। शब्द अथवा वाक्य दिखाने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
<string name="zaps">ज्साप</string>
<string name="view_count">दृष्ट संख्या</string>
<string name="boost">उद्धृत करें</string>
<string name="boosted">उद्धृत किया गया</string>
<string name="edited">सम्पादित</string>
<string name="edited_number">सम्पादन #%1$s</string>
<string name="original">मूल</string>
<string name="quote">वचन</string>
<string name="fork">पथभेद</string>
<string name="propose_an_edit">सम्पादन सुझाव दें</string>
<string name="new_amount_in_sats">साट्स में नया संख्या</string>
<string name="add">जोडें</string>
<string name="replying_to">"उत्तर दें इनको "</string>
<string name="and">" तथा "</string>
<string name="in_channel">"प्रणाली अभ्यन्तर "</string>
<string name="profile_banner">परिचय पृष्ठ चित्र</string>
<string name="payment_successful">भुगतान सफल</string>
<string name="error_parsing_error_message">अपक्रम संदेश परखने में अपक्रम</string>
<string name="following">" अनुचरण"</string>
<string name="followers">" अनुचर"</string>
<string name="profile">परिचय</string>
<string name="security_filters">सुरक्षा छलनी</string>
<string name="log_out">निर्गमनांकन</string>
<string name="show_more">अधिक दिखाएँ</string>
<string name="lightning_invoice">लैटनिंग चालान</string>
<string name="pay">भुगतान दें</string>
<string name="lightning_tips">लैटनिंग दान</string>
<string name="note_to_receiver">ग्राहक के लिए सूचना</string>
<string name="thank_you_so_much">बहुत धन्यवाद!</string>
<string name="amount_in_sats">संख्या साट्स में</string>
<string name="send_sats">साट्स भेजें</string>
<string name="error_parsing_preview_for">"%1$s का पूर्वीक्षण परखने में अपक्रम : %2$s"</string>
<string name="preview_card_image_for">"%1$s के लिए पूर्वीक्षण पत्ता चित्र"</string>
<string name="new_channel">नयी प्रणाली</string>
<string name="channel_name">प्रणाली नाम</string>
<string name="my_awesome_group">मेरा बढिया समूह</string>
<string name="picture_url">चित्र जालपता</string>
<string name="description">विवरण</string>
<string name="about_us">"हमारा परिचय.. "</string>
<string name="what_s_on_your_mind">आपके मन में क्या है।</string>
<string name="post">टीका</string>
<string name="save">अभिलेखित करें</string>
<string name="create">बनाएँ</string>
<string name="cancel">रोक दें</string>
<string name="failed_to_upload_the_image">चित्र आरोहण असफल</string>
<string name="relay_address">पुनःप्रसारक पता</string>
<string name="posts">टीकाएँ</string>
<string name="bytes">अष्टक</string>
<string name="errors">अपक्रम</string>
<string name="home_feed">मुख्य सूचनावली</string>
<string name="private_message_feed">निजी सूचनावली</string>
<string name="public_chat_feed">सार्वजनिक सूचनावली</string>
<string name="global_feed">वैश्विक सूचनावली</string>
<string name="search_feed">खोज सूचनावली</string>
<string name="add_a_relay">पुनःप्रसारक जोडें</string>
<string name="display_name">प्रदर्शन नाम</string>
<string name="my_display_name">मेरा प्रदर्शन नाम</string>
<string name="my_awesome_name">उष्ट्रपक्षी मक्बढिया</string>
<string name="welcome">स्वागतम उष्ट्रपक्षी</string>
<string name="username">उपयोगकर्ता नाम</string>
<string name="my_username">मेरा उपयोगकर्ता नाम</string>
<string name="about_me">मेरा परिचय</string>
<string name="avatar_url">अवतारचित्र जालपता</string>
<string name="banner_url">चौडाचित्र जालपता</string>
<string name="website_url">जालस्थान पता</string>
<string name="ln_address">लै॰ने॰ पता</string>
<string name="ln_url_outdated">लै॰ने॰ पता (पुराना)</string>
<string name="image_saved_to_the_gallery">चित्र को चित्रालय में जोड दिया गया</string>
<string name="failed_to_save_the_image">चित्र को अभिलेखित करने में असफल</string>
<string name="upload_image">चित्र आरोहण</string>
<string name="uploading">आरोहण चल रहा है…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">उपयोगकर्ता का कोई लैटनिंग पता स्थापित नही जिसपर साट्स प्राप्त कर सके</string>
<string name="reply_here">"उत्तर यहाँ दें.. "</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">नोस्ट्र में बाँटने के लिए टीका विभेदक का प्रतिलिपि करता है योज्यफलक में</string>
<string name="copy_channel_id_note_to_the_clipboard">प्रणाली विभेदक (टीका) का प्रतिलिपि करें योज्यफलक में</string>
<string name="edits_the_channel_metadata">प्रणाली उपतथ्य का सम्पादन करता है</string>
<string name="join">जुडें</string>
<string name="known">ज्ञात</string>
<string name="new_requests">नये अनुरोध</string>
<string name="blocked_users">अवरोधित उपयोगकर्ता</string>
<string name="new_threads">नये सूत्र</string>
<string name="conversations">संवाद</string>
<string name="notes">टीकाएँ</string>
<string name="replies">उत्तर</string>
<string name="follows">"अनुचर"</string>
<string name="reports">"सूचनाएँ"</string>
<string name="more_options">अधिक विकल्प</string>
<string name="relays">" पुनःप्रसारक"</string>
<string name="website">जालस्थान</string>
<string name="lightning_address">लैटनिंग पता</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">NSEC विभेदक (आपका गुप्त पारणशब्द) का प्रतिलिपि करता है योज्यफलक में सुरक्षा के लिए</string>
<string name="copy_private_key_to_the_clipboard">गुप्त कुंचिका का प्रतिलिपि करें योज्यफलक में</string>
<string name="copies_the_public_key_to_the_clipboard_for_sharing">ख्याप्य कुंचिका का प्रतिलिपि करता है योज्यफलक में बाँटने के लिए</string>
<string name="copy_public_key_npub_to_the_clipboard">ख्याप्य कुंचिका (NPub) का प्रतिलिपि करें योज्यफलक में</string>
<string name="send_a_direct_message">सीधा संदेश भेजें</string>
<string name="edits_the_user_s_metadata">उपयोगकर्ता के उपतथ्य का सम्पादन करता है</string>
<string name="follow">अनुसरण करें</string>
<string name="follow_back">प्रति अनुसरण करें</string>
<string name="unblock">अवरोध हटाएँ</string>
<string name="copy_user_id">उपयोगकर्ता विभेदक का प्रतिलिपि करें</string>
<string name="unblock_user">उपयोगकर्ता अवरोध हटाएँ</string>
<string name="npub_hex_username">"npub, उपयोगकर्ता नाम, लेख"</string>
<string name="clear">मार्जित करें</string>
<string name="app_logo">क्रमक चिह्न</string>
<string name="nsec_npub_hex_private_key">nsec.. अथवा npub..</string>
<string name="ncryptsec_password">पारणशब्द कुंचिका को खोलने के लिए</string>
<string name="show_password">पारणशब्द दिखाएँ</string>
<string name="hide_password">पारणशब्द छिपाएँ</string>
<string name="invalid_key">अमान्य कुंचिका</string>
<string name="invalid_key_with_message">अमान्य कुंचिका : %1$s</string>
<string name="i_accept_the">"मैं स्वीकार कर्ता हूँ "</string>
</resources>

Wyświetl plik

@ -275,6 +275,7 @@
<string name="report_dialog_title">Tiltás és Jelentés</string>
<string name="block_only">Tiltás</string>
<string name="bookmarks">Könyvjelzők</string>
<string name="drafts">Piszkozatok</string>
<string name="private_bookmarks">Privát Könyvjelzők</string>
<string name="public_bookmarks">Publikus Könyvjelzők</string>
<string name="add_to_private_bookmarks">Hozzáadás a Privát Könyvjelzőimhez</string>
@ -621,6 +622,7 @@
<string name="server_did_not_provide_a_url_after_uploading">A szerver a feltöltés után nem adott URL-t</string>
<string name="could_not_download_from_the_server">Nem sikerült a szerverről a feltöltött médiát letölteni</string>
<string name="could_not_prepare_local_file_to_upload">Nem sikerült előkészíteni a helyi fájlt a feltöltésre: %1$s</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>
<string name="route_home">Főoldal</string>
@ -692,4 +694,10 @@
<string name="accessibility_play_username">Felhasználónév lejátszása hangként</string>
<string name="accessibility_scan_qr_code">QR-kód beolvasása</string>
<string name="accessibility_navigate_to_alby">Keresd fel a harmadik féltől származó pénztárcaszolgáltatót, az Alby-t</string>
<string name="it_s_not_possible_to_reply_to_a_draft_note">A bejegyzésvázlatra nem lehet válaszolni</string>
<string name="it_s_not_possible_to_quote_to_a_draft_note">A bejegyzésvázlatot nem lehet idézni</string>
<string name="it_s_not_possible_to_react_to_a_draft_note">A bejegyzésvázlatra nem lehet reagálni</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">A bejegyzésvázlatra nem lehet zap-pelni</string>
<string name="draft_note">Bejegyzéspiszkozat</string>
<string name="load_from_text">Tőle üzenet</string>
</resources>

Wyświetl plik

@ -700,4 +700,5 @@
<string name="it_s_not_possible_to_react_to_a_draft_note">Het is niet mogelijk om op een concept te reageren</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">Het is niet mogelijk om een concept op te zappen</string>
<string name="draft_note">Concept notitie</string>
<string name="load_from_text">Van Msg</string>
</resources>

Wyświetl plik

@ -659,11 +659,22 @@
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s is correct.\n\nException was: %3$s</string>
<string name="could_not_fetch_invoice_from">Could not fetch invoice from %1$s</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">Error Parsing JSON from Lightning Address. Check the user\'s lightning setup</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user">Error Parsing JSON from %1$s. Check the user\'s lightning setup</string>
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration">Callback URL not found in the User\'s lightning address server configuration</string>
<string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user">Callback URL not found from %1$s\'s response</string>
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup">Error Parsing JSON from Lightning Address\'s invoice fetch. Check the user\'s lightning setup</string>
<string name="error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user">Error Parsing JSON from %1$s\'s invoice fetch. Check the user\'s lightning setup</string>
<string name="incorrect_invoice_amount_sats_from_it_should_have_been">Incorrect invoice amount (%1$s sats) from %2$s. It should have been %3$s</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error">Unable to create a lightning invoice before sending the zap. The receiver\'s lightning wallet sent the following error: %1$s</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Unable to create a lightning invoice. Message from %1$s: %2$s</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json">Unable to create a lightning invoice before sending the zap. Element pr not found in the resulting JSON.</string>
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Unable to create a lightning invoice from %1$s: Element pr not found in the resulting JSON.</string>
<string name="read_only_user">Read-only user</string>
<string name="no_reactions_setup">No reactions setup</string>

Wyświetl plik

@ -60,7 +60,7 @@ class NewMessageTaggerKeyParseTest {
"1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605",
(result?.key?.entity as? Nip19Bech32.Note)?.hex,
)
assertEquals("", result?.restOfWord)
assertEquals(null, result?.restOfWord)
}
@Test
@ -73,7 +73,7 @@ class NewMessageTaggerKeyParseTest {
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
(result?.key?.entity as? Nip19Bech32.NPub)?.hex,
)
assertEquals("", result?.restOfWord)
assertEquals(null, result?.restOfWord)
}
@Test

Wyświetl plik

@ -38,7 +38,7 @@ android {
}
composeOptions {
// Should match compose version : https://developer.android.com/jetpack/androidx/releases/compose-kotlin
kotlinCompilerExtensionVersion "1.5.8"
kotlinCompilerExtensionVersion "1.5.11"
}
}

Wyświetl plik

@ -26,35 +26,49 @@ import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.Event
class DeletionIndex {
data class DeletionRequest(val reference: String, val publicKey: HexKey)
data class DeletionRequest(val reference: String, val publicKey: HexKey) : Comparable<DeletionRequest> {
override fun compareTo(other: DeletionRequest): Int {
val compared = reference.compareTo(other.reference)
return if (compared == 0) {
publicKey.compareTo(publicKey)
} else {
compared
}
}
}
// stores a set of id OR atags (kind:pubkey:dtag) by pubkey with the created at of the deletion event.
// Anything newer than the date should not be deleted.
private val deletedReferencesBefore = LargeCache<DeletionRequest, Long>()
fun add(event: DeletionEvent) {
fun add(event: DeletionEvent): Boolean {
var atLeastOne = false
event.tags.forEach {
if (it.size > 1 && (it[0] == "a" || it[0] == "e")) {
add(it[1], event.createdAt, event.pubKey)
if (add(it[1], event.pubKey, event.createdAt)) {
atLeastOne = true
}
}
}
return atLeastOne
}
private fun add(
ref: String,
createdAt: Long,
byPubKey: HexKey,
) {
createdAt: Long,
): Boolean {
val key = DeletionRequest(ref, byPubKey)
val deletionTime = deletedReferencesBefore.get(key)
if (deletionTime == null) {
val previousDeletionTime = deletedReferencesBefore.get(key)
if (previousDeletionTime == null || createdAt > previousDeletionTime) {
deletedReferencesBefore.put(key, createdAt)
} else {
// updates with newer deletion.
if (createdAt > deletionTime) {
deletedReferencesBefore.put(key, createdAt)
}
return true
}
return false
}
fun hasBeenDeleted(event: Event): Boolean {
@ -62,7 +76,7 @@ class DeletionIndex {
if (hasBeenDeleted(key)) return true
if (event is AddressableEvent) {
if (hasBeenDeleted(key, event.createdAt)) return true
if (hasBeenDeleted(DeletionRequest(event.addressTag(), event.pubKey), event.createdAt)) return true
}
return false
@ -75,6 +89,6 @@ class DeletionIndex {
createdAt: Long,
): Boolean {
val deletionTime = deletedReferencesBefore.get(key)
return deletionTime != null && createdAt < deletionTime
return deletionTime != null && createdAt <= deletionTime
}
}

Wyświetl plik

@ -1,7 +1,7 @@
[versions]
accompanistAdaptive = "0.34.0"
activityCompose = "1.8.2"
agp = "8.3.1"
agp = "8.3.2"
androidKotlinGeohash = "1.0"
androidLifecycle = "2.7.0"
androidxJunit = "1.2.0-alpha03"
@ -12,7 +12,7 @@ benchmarkJunit4 = "1.2.3"
biometricKtx = "1.2.0-alpha05"
blurhash = "1.0.0"
coil = "2.6.0"
composeBom = "2024.03.00"
composeBom = "2024.04.00"
coreKtx = "1.12.0"
espressoCore = "3.5.1"
firebaseBom = "32.8.0"
@ -21,12 +21,12 @@ gms = "4.4.1"
jacksonModuleKotlin = "2.17.0"
jna = "5.14.0"
junit = "4.13.2"
kotlin = "1.9.22"
kotlin = "1.9.23"
kotlinxCollectionsImmutable = "0.3.7"
languageId = "17.0.5"
lazysodiumAndroid = "5.1.0"
lightcompressor = "1.3.2"
markdown = "48702a8ced"
markdown = "077a2cde64"
media3 = "1.3.0"
mockk = "1.13.10"
navigationCompose = "2.7.7"

Wyświetl plik

@ -53,7 +53,7 @@ object Nip19Bech32 {
)
@Immutable
data class ParseReturn(val entity: Entity, val additionalChars: String = "")
data class ParseReturn(val entity: Entity, val additionalChars: String? = null)
interface Entity
@ -96,7 +96,7 @@ object Nip19Bech32 {
if (type == null) return null
return parseComponents(type, key, additionalChars)
return parseComponents(type, key, additionalChars.ifEmpty { null })
} catch (e: Throwable) {
Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e)
}
@ -123,7 +123,7 @@ object Nip19Bech32 {
"nembed1" -> nembed(bytes)
else -> null
}?.let {
ParseReturn(it, additionalChars ?: "")
ParseReturn(it, additionalChars)
}
} catch (e: Throwable) {
Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e)

Wyświetl plik

@ -38,6 +38,8 @@ class DeletionEvent(
fun deleteAddresses() = taggedAddresses()
fun deleteAddressTags() = tags.mapNotNull { if (it.size > 1 && it[0] == "a") it[1] else null }
companion object {
const val KIND = 5
const val ALT = "Deletion event"

Wyświetl plik

@ -115,7 +115,7 @@ class DraftEvent(
pubKey: HexKey,
dTag: String,
): String {
return ATag(KIND, pubKey, dTag, null).toTag()
return ATag.assembleATag(KIND, pubKey, dTag)
}
fun create(

Wyświetl plik

@ -97,9 +97,9 @@ class Result(
class ExternalSignerLauncher(
private val npub: String,
val signerPackageName: String = "com.greenart7c3.nostrsigner",
val signerPackageName: String,
) {
private val contentCache = LruCache<String, (String) -> Unit>(20)
private val contentCache = LruCache<String, (String) -> Unit>(50)
private var signerAppLauncher: ((Intent) -> Unit)? = null
private var contentResolver: (() -> ContentResolver)? = null
@ -125,20 +125,20 @@ class ExternalSignerLauncher(
val localResults: Array<Result> = Result.fromJsonArray(results)
localResults.forEach {
val signature = it.signature ?: ""
val packageName = it.`package` ?: ""
val packageName = it.`package`?.let { "-$it" } ?: ""
val id = it.id ?: ""
if (id.isNotBlank()) {
val result = if (packageName.isNotBlank()) "$signature-$packageName" else signature
val result = if (packageName.isNotBlank()) "$signature$packageName" else signature
val contentCache = contentCache.get(id)
contentCache?.invoke(result)
}
}
} else {
val signature = data.getStringExtra("signature") ?: ""
val packageName = data.getStringExtra("package") ?: ""
val packageName = data.getStringExtra("package")?.let { "-$it" } ?: ""
val id = data.getStringExtra("id") ?: ""
if (id.isNotBlank()) {
val result = if (packageName.isNotBlank()) "$signature-$packageName" else signature
val result = if (packageName.isNotBlank()) "$signature$packageName" else signature
val contentCache = contentCache.get(id)
contentCache?.invoke(result)
}
@ -246,13 +246,11 @@ class ExternalSignerLauncher(
columnName: String = "signature",
onReady: (String) -> Unit,
) {
val result =
getDataFromResolver(
SignerType.SIGN_EVENT,
arrayOf(event.toJson(), event.pubKey()),
columnName,
)
result.fold(
getDataFromResolver(
SignerType.SIGN_EVENT,
arrayOf(event.toJson(), event.pubKey()),
columnName,
).fold(
onFailure = { },
onSuccess = {
if (it == null) {
@ -286,7 +284,7 @@ class ExternalSignerLauncher(
): kotlin.Result<String?> {
val localData =
if (signerType !== SignerType.GET_PUBLIC_KEY) {
data.toList().plus(npub).toTypedArray()
arrayOf(*data, npub)
} else {
data
}
@ -326,15 +324,24 @@ class ExternalSignerLauncher(
return kotlin.Result.success(null)
}
fun hashCodeFields(
str1: String,
str2: String,
onReady: (String) -> Unit,
): Int {
var result = str1.hashCode()
result = 31 * result + str2.hashCode()
result = 31 * result + onReady.hashCode()
return result
}
fun decrypt(
encryptedContent: String,
pubKey: HexKey,
signerType: SignerType = SignerType.NIP04_DECRYPT,
onReady: (String) -> Unit,
) {
val id = (encryptedContent + pubKey + onReady.toString()).hashCode().toString()
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
result.fold(
getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey)).fold(
onFailure = { },
onSuccess = {
if (it == null) {
@ -342,7 +349,7 @@ class ExternalSignerLauncher(
encryptedContent,
signerType,
pubKey,
id,
hashCodeFields(encryptedContent, pubKey, onReady).toString(),
onReady,
)
} else {
@ -358,9 +365,7 @@ class ExternalSignerLauncher(
signerType: SignerType = SignerType.NIP04_ENCRYPT,
onReady: (String) -> Unit,
) {
val id = (decryptedContent + pubKey + onReady.toString()).hashCode().toString()
val result = getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey))
result.fold(
getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey)).fold(
onFailure = { },
onSuccess = {
if (it == null) {
@ -368,7 +373,7 @@ class ExternalSignerLauncher(
decryptedContent,
signerType,
pubKey,
id,
hashCodeFields(decryptedContent, pubKey, onReady).toString(),
onReady,
)
} else {
@ -382,9 +387,7 @@ class ExternalSignerLauncher(
event: LnZapRequestEvent,
onReady: (String) -> Unit,
) {
val result =
getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey))
result.fold(
getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey)).fold(
onFailure = { },
onSuccess = {
if (it == null) {

Wyświetl plik

@ -21,10 +21,9 @@
package com.vitorpamplona.quartz.signers
import android.util.Log
import com.goterl.lazysodium.BuildConfig
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventFactory
import com.vitorpamplona.quartz.events.LnZapPrivateEvent
@ -32,7 +31,7 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent
class NostrSignerExternal(
pubKey: HexKey,
val launcher: ExternalSignerLauncher = ExternalSignerLauncher(pubKey.hexToByteArray().toNpub()),
val launcher: ExternalSignerLauncher,
) : NostrSigner(pubKey) {
override fun <T : Event> sign(
createdAt: Long,
@ -78,7 +77,7 @@ class NostrSignerExternal(
event.kind,
event.tags,
event.content,
signature,
signature.split("-")[0],
) as? T?
)
?.let { onReady(it) }
@ -91,9 +90,11 @@ class NostrSignerExternal(
toPublicKey: HexKey,
onReady: (String) -> Unit,
) {
Log.d("NostrExternalSigner", "Encrypt NIP04 Event: $decryptedContent")
if (BuildConfig.DEBUG) {
Log.d("NostrExternalSigner", "Encrypt NIP04 Event: $decryptedContent")
}
return launcher.encrypt(
launcher.encrypt(
decryptedContent,
toPublicKey,
SignerType.NIP04_ENCRYPT,
@ -106,9 +107,11 @@ class NostrSignerExternal(
fromPublicKey: HexKey,
onReady: (String) -> Unit,
) {
Log.d("NostrExternalSigner", "Decrypt NIP04 Event: $encryptedContent")
if (BuildConfig.DEBUG) {
Log.d("NostrExternalSigner", "Decrypt NIP04 Event: $encryptedContent")
}
return launcher.decrypt(
launcher.decrypt(
encryptedContent,
fromPublicKey,
SignerType.NIP04_DECRYPT,
@ -121,9 +124,11 @@ class NostrSignerExternal(
toPublicKey: HexKey,
onReady: (String) -> Unit,
) {
Log.d("NostrExternalSigner", "Encrypt NIP44 Event: $decryptedContent")
if (BuildConfig.DEBUG) {
Log.d("NostrExternalSigner", "Encrypt NIP44 Event: $decryptedContent")
}
return launcher.encrypt(
launcher.encrypt(
decryptedContent,
toPublicKey,
SignerType.NIP44_ENCRYPT,
@ -136,9 +141,11 @@ class NostrSignerExternal(
fromPublicKey: HexKey,
onReady: (String) -> Unit,
) {
Log.d("NostrExternalSigner", "Decrypt NIP44 Event: $encryptedContent")
if (BuildConfig.DEBUG) {
Log.d("NostrExternalSigner", "Decrypt NIP44 Event: $encryptedContent")
}
return launcher.decrypt(
launcher.decrypt(
encryptedContent,
fromPublicKey,
SignerType.NIP44_DECRYPT,
@ -150,7 +157,7 @@ class NostrSignerExternal(
event: LnZapRequestEvent,
onReady: (LnZapPrivateEvent) -> Unit,
) {
return launcher.decryptZapEvent(event) { jsonEvent ->
launcher.decryptZapEvent(event) { jsonEvent ->
try {
(Event.fromJson(jsonEvent) as? LnZapPrivateEvent)?.let { onReady(it) }
} catch (e: Exception) {