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"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" /> <option name="version" value="1.9.23" />
</component> </component>
</project> </project>

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -171,6 +171,22 @@ object LocalCache {
return channels.get(key) 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? { fun checkGetOrCreateNote(key: String): Note? {
checkNotInMainThread() checkNotInMainThread()
@ -959,53 +975,53 @@ object LocalCache {
} }
fun consume(event: DeletionEvent) { 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() deletedAtLeastOne = true
.mapNotNull { getNoteIfExists(it) } }
.forEach { deleteNote -> }
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey) {
// reverts the add
deleteNote(deleteNote)
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() if (deletedAtLeastOne) {
val addressSet = addressList.toSet() val note = Note(event.id)
note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList())
addressList refreshObservers(note)
.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
}
} }
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) refreshObservers(note)
} }
private fun consume( fun consume(
event: SealedGossipEvent, event: SealedGossipEvent,
relay: Relay?, relay: Relay?,
) { ) {
@ -2098,7 +2114,7 @@ object LocalCache {
} }
} }
private fun consume( fun consume(
event: DraftEvent, event: DraftEvent,
relay: Relay?, relay: Relay?,
) { ) {
@ -2314,7 +2330,14 @@ object LocalCache {
} }
fun hasConsumed(notificationEvent: Event): Boolean { 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>() var proxyChangeListeners = ArrayList<() -> Unit>()
private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI
private var defaultHttpClient: OkHttpClient? = null private var defaultHttpClient: OkHttpClient? = null
private var defaultHttpClientWithoutProxy: OkHttpClient? = null
// fires off every time value of the property changes // fires off every time value of the property changes
private var internalProxy: Proxy? by private var internalProxy: Proxy? by
@ -58,6 +59,10 @@ object HttpClientManager {
} }
} }
fun getDefaultProxy(): Proxy? {
return this.internalProxy
}
fun setDefaultTimeout(timeout: Duration) { fun setDefaultTimeout(timeout: Duration) {
Log.d("HttpClient", "Changing timeout to: $timeout") Log.d("HttpClient", "Changing timeout to: $timeout")
if (this.defaultTimeout.seconds != timeout.seconds) { if (this.defaultTimeout.seconds != timeout.seconds) {
@ -98,11 +103,18 @@ object HttpClientManager {
} }
} }
fun getHttpClient(): OkHttpClient { fun getHttpClient(useProxy: Boolean = true): OkHttpClient {
if (this.defaultHttpClient == null) { return if (useProxy) {
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout) 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( fun initProxy(

Wyświetl plik

@ -121,8 +121,9 @@ class Nip11Retriever {
try { try {
val request: Request = val request: Request =
Request.Builder().header("Accept", "application/nostr+json").url(url).build() 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) .newCall(request)
.enqueue( .enqueue(
object : Callback { object : Callback {

Wyświetl plik

@ -268,42 +268,69 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
// Avoid decrypting over and over again if the event already exist. // Avoid decrypting over and over again if the event already exist.
if (!event.isDeleted()) { if (!event.isDeleted()) {
val note = LocalCache.getNoteIfExists(event.id) val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
if (note != null && relay.brief in note.relays) return 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 LocalCache.justConsume(event, relay)
event.cachedDraft(account.signer) {} }
LocalCache.justConsume(event, relay)
} }
} }
is GiftWrapEvent -> { is GiftWrapEvent -> {
// Avoid decrypting over and over again if the event already exist. // Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id) val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return val noteEvent = note?.event as? GiftWrapEvent
if (noteEvent != null) {
event.cachedGift(account.signer) { this.consume(it, relay) } 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 -> { is SealedGossipEvent -> {
// Avoid decrypting over and over again if the event already exist. // Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id) val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return val noteEvent = note?.event as? SealedGossipEvent
if (noteEvent != null) {
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) } 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 -> { is LnZapEvent -> {
// Avoid decrypting over and over again if the event already exist. // Avoid decrypting over and over again if the event already exist.
val note = LocalCache.getNoteIfExists(event.id) val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return if (note?.event == null) {
event.zapRequest?.let {
event.zapRequest?.let { if (it.isPrivateZap()) {
if (it.isPrivateZap()) { it.decryptPrivateZap(account.signer) {}
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.Channel
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.PublicChatChannel 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.FeedType
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
@ -63,7 +63,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
// downloads linked events to this event. // downloads linked events to this event.
return TypedFilter( return TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = listOf(ChannelCreateEvent.KIND), kinds = listOf(ChannelCreateEvent.KIND),
@ -86,7 +86,7 @@ object NostrSingleChannelDataSource : NostrDataSource("SingleChannelFeed") {
return directEventsToLoad.map { return directEventsToLoad.map {
it.address().let { aTag -> it.address().let { aTag ->
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = listOf(aTag.kind), 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.AddressableNote
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User 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.EOSETime
import com.vitorpamplona.amethyst.service.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
@ -60,7 +60,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return groupByEOSEPresence(addressesToWatch).map { return groupByEOSEPresence(addressesToWatch).map {
listOf( listOf(
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = kinds =
@ -82,7 +82,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
), ),
), ),
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = kinds =
@ -110,7 +110,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
it.address()?.let { aTag -> it.address()?.let { aTag ->
if (aTag.kind < 25000 && aTag.dTag.isBlank()) { if (aTag.kind < 25000 && aTag.dTag.isBlank()) {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = listOf(aTag.kind), kinds = listOf(aTag.kind),
@ -120,7 +120,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
) )
} else { } else {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = listOf(aTag.kind), kinds = listOf(aTag.kind),
@ -142,7 +142,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return groupByEOSEPresence(eventsToWatch).map { return groupByEOSEPresence(eventsToWatch).map {
listOf( listOf(
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = kinds =
@ -165,7 +165,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
), ),
), ),
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = kinds =
@ -190,9 +190,10 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return groupByEOSEPresence(eventsToWatch).map { return groupByEOSEPresence(eventsToWatch).map {
listOf( listOf(
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = listOf(TextNoteEvent.KIND),
tags = mapOf("q" to it.map { it.idHex }), tags = mapOf("q" to it.map { it.idHex }),
since = findMinimumEOSEs(it), since = findMinimumEOSEs(it),
// Max amount of "replies" to download on a specific event. // Max amount of "replies" to download on a specific event.
@ -221,7 +222,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
// downloads linked events to this event. // downloads linked events to this event.
return listOf( return listOf(
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
ids = interestedEvents.toList(), 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.model.User
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
import com.vitorpamplona.amethyst.service.relays.EOSETime 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.JsonFilter
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.MetadataEvent
@ -64,7 +65,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
val minEOSEs = findMinimumEOSEsForUsers(group) val minEOSEs = findMinimumEOSEsForUsers(group)
listOf( listOf(
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND), kinds = listOf(MetadataEvent.KIND, StatusEvent.KIND),
@ -73,7 +74,7 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
), ),
), ),
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, types = EVENT_FINDER_TYPES,
filter = filter =
JsonFilter( JsonFilter(
kinds = listOf(ReportEvent.KIND), 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.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Lud06 import com.vitorpamplona.quartz.encoders.Lud06
import com.vitorpamplona.quartz.encoders.toLnUrl
import okhttp3.Request import okhttp3.Request
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode 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( fun lnAddressInvoice(
lnaddress: String, lnaddress: String,
milliSats: Long, milliSats: Long,
@ -190,7 +175,8 @@ class LightningAddressResolver() {
onError( onError(
context.getString(R.string.error_unable_to_fetch_invoice), context.getString(R.string.error_unable_to_fetch_invoice),
context.getString( 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 null
@ -202,7 +188,8 @@ class LightningAddressResolver() {
onError( onError(
context.getString(R.string.error_unable_to_fetch_invoice), context.getString(R.string.error_unable_to_fetch_invoice),
context.getString( 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_unable_to_fetch_invoice),
context.getString( context.getString(
R.string 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 null
@ -268,7 +256,8 @@ class LightningAddressResolver() {
context.getString(R.string.error_unable_to_fetch_invoice), context.getString(R.string.error_unable_to_fetch_invoice),
context.getString( context.getString(
R.string 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, reason,
), ),
) )
@ -279,7 +268,8 @@ class LightningAddressResolver() {
context.getString(R.string.error_unable_to_fetch_invoice), context.getString(R.string.error_unable_to_fetch_invoice),
context.getString( context.getString(
R.string 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.app.NotificationManager
import android.content.Context import android.content.Context
import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
@ -45,6 +46,7 @@ import java.math.BigDecimal
class EventNotificationConsumer(private val applicationContext: Context) { class EventNotificationConsumer(private val applicationContext: Context) {
suspend fun consume(event: GiftWrapEvent) { suspend fun consume(event: GiftWrapEvent) {
Log.d("EventNotificationConsumer", "New Notification Arrived")
if (!LocalCache.justVerify(event)) return if (!LocalCache.justVerify(event)) return
if (!notificationManager().areNotificationsEnabled()) return if (!notificationManager().areNotificationsEnabled()) return
@ -64,14 +66,23 @@ class EventNotificationConsumer(private val applicationContext: Context) {
account: Account, account: Account,
) { ) {
pushWrappedEvent.cachedGift(account.signer) { notificationEvent -> 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 -> unwrapAndConsume(notificationEvent, account) { innerEvent ->
if (!LocalCache.hasConsumed(innerEvent)) {
Log.d("EventNotificationConsumer", "Unwrapped consume $consumed ${innerEvent.javaClass.simpleName}")
if (!consumed) {
if (innerEvent is PrivateDmEvent) { if (innerEvent is PrivateDmEvent) {
Log.d("EventNotificationConsumer", "New Nip-04 DM to Notify")
notify(innerEvent, account) notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) { } else if (innerEvent is LnZapEvent) {
Log.d("EventNotificationConsumer", "New Zap to Notify")
notify(innerEvent, account) notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) { } else if (innerEvent is ChatMessageEvent) {
Log.d("EventNotificationConsumer", "New ChatMessage to Notify")
notify(innerEvent, account) notify(innerEvent, account)
} }
} }
@ -86,6 +97,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
onReady: (Event) -> Unit, onReady: (Event) -> Unit,
) { ) {
if (!LocalCache.justVerify(event)) return if (!LocalCache.justVerify(event)) return
if (LocalCache.hasConsumed(event)) return
when (event) { when (event) {
is GiftWrapEvent -> { is GiftWrapEvent -> {
@ -93,9 +105,11 @@ class EventNotificationConsumer(private val applicationContext: Context) {
} }
is SealedGossipEvent -> { is SealedGossipEvent -> {
event.cachedGossip(account.signer) { event.cachedGossip(account.signer) {
// this is not verifiable if (!LocalCache.hasConsumed(it)) {
LocalCache.justConsume(it, null) // this is not verifiable
onReady(it) LocalCache.justConsume(it, null)
onReady(it)
}
} }
} }
else -> { else -> {

Wyświetl plik

@ -50,6 +50,9 @@ enum class FeedType {
val COMMON_FEED_TYPES = val COMMON_FEED_TYPES =
setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL) 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( class Relay(
val url: String, val url: String,
val read: Boolean = true, val read: Boolean = true,
@ -63,7 +66,12 @@ class Relay(
const val RECONNECTING_IN_SECONDS = 60 * 3 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 listeners = setOf<Listener>()
private var socket: WebSocket? = null private var socket: WebSocket? = null

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -61,25 +61,7 @@ fun LoadUrlPreview(
) { state -> ) { state ->
when (state) { when (state) {
is UrlPreviewState.Loaded -> { is UrlPreviewState.Loaded -> {
if (state.previewInfo.mimeType.type == "image") { RenderLoaded(state, url, accountViewModel)
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)
}
} }
else -> { else -> {
ClickableUrl(urlText, url) 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.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider 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.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection 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.SpanStyle
import androidx.compose.ui.text.TextMeasurer import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle 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.dp
import androidx.compose.ui.unit.em import androidx.compose.ui.unit.em
import androidx.lifecycle.viewmodel.compose.viewModel 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.compose.produceCachedState
import com.vitorpamplona.amethyst.commons.richtext.BechSegment import com.vitorpamplona.amethyst.commons.richtext.BechSegment
import com.vitorpamplona.amethyst.commons.richtext.CashuSegment 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.ImageSegment
import com.vitorpamplona.amethyst.commons.richtext.InvoiceSegment import com.vitorpamplona.amethyst.commons.richtext.InvoiceSegment
import com.vitorpamplona.amethyst.commons.richtext.LinkSegment 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.PhoneSegment
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment 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.RichTextViewerState
import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment
import com.vitorpamplona.amethyst.commons.richtext.Segment 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.User
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.service.CachedRichTextParser 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.LoadUser
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.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.innerPostModifier
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
import com.vitorpamplona.amethyst.ui.uriToRoute
import com.vitorpamplona.quartz.crypto.KeyPair 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.EmptyTagList
import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.ImmutableListOfLists
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
fun isMarkdown(content: String): Boolean { fun isMarkdown(content: String): Boolean {
return content.startsWith("> ") || return content.startsWith("> ") ||
content.startsWith("# ") || content.startsWith("# ") ||
content.contains("##") || content.contains("##") ||
content.contains("__") || content.contains("__") ||
content.contains("**") ||
content.contains("```") || content.contains("```") ||
content.contains("](") content.contains("](")
} }
@ -137,7 +125,7 @@ fun RichTextViewer(
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
if (remember(content) { isMarkdown(content) }) { if (remember(content) { isMarkdown(content) }) {
RenderContentAsMarkdown(content, tags, accountViewModel, nav) RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
} else { } else {
RenderRegular(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav) 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 @Composable
fun BechLink( fun BechLink(
word: String, word: String,
@ -683,7 +480,7 @@ fun BechLink(
} }
@Composable @Composable
private fun DisplayFullNote( fun DisplayFullNote(
note: Note, note: Note,
extraChars: String?, extraChars: String?,
quotesLeft: Int, quotesLeft: Int,
@ -752,13 +549,7 @@ fun HashTag(
@Composable @Composable
private fun InlineIcon(hashtagIcon: HashtagIcon) = private fun InlineIcon(hashtagIcon: HashtagIcon) =
InlineTextContent( InlineTextContent(inlinePlaceholder) {
Placeholder(
width = Font17SP,
height = Font17SP,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
),
) {
Icon( Icon(
imageVector = hashtagIcon.icon, imageVector = hashtagIcon.icon,
contentDescription = hashtagIcon.description, contentDescription = hashtagIcon.description,

Wyświetl plik

@ -141,6 +141,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable import net.engawapg.lib.zoomable.zoomable
@ -542,7 +543,10 @@ fun ShowHash(
if (content.hash != null) { if (content.hash != null) {
LaunchedEffect(key1 = content.url) { LaunchedEffect(key1 = content.url) {
val newVerifiedHash = verifyHash(content) val newVerifiedHash =
withContext(Dispatchers.IO) {
verifyHash(content)
}
if (newVerifiedHash != verifiedHash) { if (newVerifiedHash != verifiedHash) {
verifiedHash = newVerifiedHash 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 wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) } var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToSetCustomZap 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 var wantsToPay by
remember(baseNote) { remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>( mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -972,7 +972,7 @@ fun ZapReaction(
onError = { _, message -> onError = { _, message ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = message showErrorMessageDialog = showErrorMessageDialog + message
} }
}, },
onPayViaIntent = { wantsToPay = it }, onPayViaIntent = { wantsToPay = it },
@ -998,7 +998,7 @@ fun ZapReaction(
onError = { _, message -> onError = { _, message ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = message showErrorMessageDialog = showErrorMessageDialog + message
} }
}, },
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } }, 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( ErrorMessageDialog(
title = stringResource(id = R.string.error_dialog_zap_error), title = stringResource(id = R.string.error_dialog_zap_error),
textContent = showErrorMessageDialog ?: "", textContent = msg,
onClickStartMessage = { onClickStartMessage = {
baseNote.author?.let { baseNote.author?.let {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel) val route = routeToMessage(it, msg, accountViewModel)
nav(route) nav(route)
} }
} }
}, },
onDismiss = { showErrorMessageDialog = null }, onDismiss = { showErrorMessageDialog = emptyList() },
) )
} }
@ -1038,7 +1039,7 @@ fun ZapReaction(
wantsToPay = persistentListOf() wantsToPay = persistentListOf()
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = it showErrorMessageDialog = showErrorMessageDialog + it
} }
}, },
) )
@ -1050,7 +1051,7 @@ fun ZapReaction(
onError = { _, message -> onError = { _, message ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = message showErrorMessageDialog = showErrorMessageDialog + message
} }
}, },
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } }, onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },

Wyświetl plik

@ -424,18 +424,17 @@ fun UpdateZapAmountDialog(
Modifier.weight(1f), Modifier.weight(1f),
) )
/* TODO: Find a way to open this in the PWA IconButton(onClick = {
IconButton(onClick = { onClose()
onClose() runCatching { uri.openUri("https://app.mutinywallet.com/settings/connections?name=Amethyst") }
runCatching { uri.openUri("https://app.mutinywallet.com/settings/connections?callbackUri=nostr+walletconnect&name=Amethyst") } }) {
}) { Icon(
Icon( painter = painterResource(R.mipmap.mutiny),
painter = painterResource(R.mipmap.mutiny), null,
null, modifier = Modifier.size(24.dp),
modifier = Modifier.size(24.dp), tint = Color.Unspecified,
tint = Color.Unspecified )
) }
}*/
IconButton( IconButton(
onClick = { 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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
@ -361,7 +363,7 @@ fun PayViaIntentDialog(
), ),
) { ) {
Surface { Surface {
Column(modifier = Modifier.padding(10.dp)) { Column(modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState())) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,

Wyświetl plik

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

Wyświetl plik

@ -48,6 +48,7 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text 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.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.FragmentActivity 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.RichTextStyle
import com.halilibo.richtext.ui.material3.Material3RichText import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.resolveDefaults import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
@ -140,12 +143,18 @@ fun AccountBackupDialog(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, 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(), style = RichTextStyle().resolveDefaults(),
renderer = null,
) { ) {
Markdown( BasicMarkdown(astNode1)
content = stringResource(R.string.account_backup_tips2_md),
)
} }
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
@ -154,12 +163,18 @@ fun AccountBackupDialog(
Spacer(modifier = Modifier.height(30.dp)) 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(), style = RichTextStyle().resolveDefaults(),
renderer = null,
) { ) {
Markdown( BasicMarkdown(astNode)
content = stringResource(R.string.account_backup_tips3_md),
)
} }
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
@ -312,7 +327,7 @@ private fun EncryptNSecCopyButton(
} }
} }
Button( OutlinedButton(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
authenticate( authenticate(
@ -324,15 +339,10 @@ private fun EncryptNSecCopyButton(
) )
}, },
shape = ButtonBorder, shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
contentPadding = ButtonPadding, contentPadding = ButtonPadding,
enabled = password.value.text.isNotBlank(), enabled = password.value.text.isNotBlank(),
) { ) {
Icon( Icon(
tint = MaterialTheme.colorScheme.onPrimary,
imageVector = Icons.Default.Key, imageVector = Icons.Default.Key,
contentDescription = contentDescription =
stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup), stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup),
@ -340,7 +350,6 @@ private fun EncryptNSecCopyButton(
) )
Text( Text(
stringResource(id = R.string.encrypt_and_copy_my_secret_key), 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.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.Dao
import com.vitorpamplona.amethyst.ui.components.BundledInsert 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.components.UrlPreviewState
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.navigation.bottomNavigationItems 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.Event
import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.Participant 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( fun checkIsOnline(
media: String?, media: String?,
onDone: (Boolean) -> Unit, 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.NEvent -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } }
is Nip19Bech32.NEmbed -> is Nip19Bech32.NEmbed ->
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
accountViewModel.loadNEmbedIfNeeded(parsed.event) val baseNote = LocalCache.getOrCreateNote(parsed.event)
if (baseNote.event == null) {
LocalCache.checkGetOrCreateNote(parsed.event.id)?.let { note -> launch(Dispatchers.IO) {
returningNote = note LocalCache.verifyAndConsume(parsed.event, null)
}
} }
returningNote = baseNote
} }
is Nip19Bech32.NRelay -> {} is Nip19Bech32.NRelay -> {}
is Nip19Bech32.NAddress -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.atag)?.let { note -> returningNote = note } } 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -47,8 +48,10 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
import com.halilibo.richtext.ui.material3.Material3RichText 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.R
import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
@ -112,12 +115,18 @@ fun ConnectOrbotDialog(
) )
Row { Row {
Material3RichText( val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown)
val astNode1 =
remember {
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
}
RichText(
style = myMarkDownStyle, style = myMarkDownStyle,
renderer = null,
) { ) {
Markdown( BasicMarkdown(astNode1)
content = stringResource(R.string.connect_through_your_orbot_setup_markdown),
)
} }
} }

Wyświetl plik

@ -36,6 +36,8 @@ import androidx.compose.material3.Shapes
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
val Shapes = val Shapes =
@ -231,3 +233,10 @@ val liveStreamTag =
val chatAuthorBox = Modifier.size(20.dp) val chatAuthorBox = Modifier.size(20.dp)
val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape) val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape)
val AuthorInfoVideoFeed = Modifier.width(75.dp).padding(end = 15.dp) 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 MarkdownTextStyle = TextStyle(lineHeight = 1.30.em)
val DefaultParagraphSpacing: TextUnit = 16.sp val DefaultParagraphSpacing: TextUnit = 18.sp
internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle -> internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->
when (level) { when (level) {

Wyświetl plik

@ -276,6 +276,7 @@
<string name="report_dialog_title">Bloquear y reportar</string> <string name="report_dialog_title">Bloquear y reportar</string>
<string name="block_only">Bloquear</string> <string name="block_only">Bloquear</string>
<string name="bookmarks">Marcadores</string> <string name="bookmarks">Marcadores</string>
<string name="drafts">Borradores</string>
<string name="private_bookmarks">Marcadores privados</string> <string name="private_bookmarks">Marcadores privados</string>
<string name="public_bookmarks">Marcadores públicos</string> <string name="public_bookmarks">Marcadores públicos</string>
<string name="add_to_private_bookmarks">Agregar a marcadores privados</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"?> <?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="report_dialog_title">Tiltás és Jelentés</string>
<string name="block_only">Tiltás</string> <string name="block_only">Tiltás</string>
<string name="bookmarks">Könyvjelzők</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="private_bookmarks">Privát Könyvjelzők</string>
<string name="public_bookmarks">Publikus 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> <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="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_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="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="login_with_qr_code">Bejelentkezés QR kóddal</string>
<string name="route">Útvonal</string> <string name="route">Útvonal</string>
<string name="route_home">Főoldal</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_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_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="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> </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_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="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="draft_note">Concept notitie</string>
<string name="load_from_text">Van Msg</string>
</resources> </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_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="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">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">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">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="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">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">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="read_only_user">Read-only user</string>
<string name="no_reactions_setup">No reactions setup</string> <string name="no_reactions_setup">No reactions setup</string>

Wyświetl plik

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

Wyświetl plik

@ -38,7 +38,7 @@ android {
} }
composeOptions { composeOptions {
// Should match compose version : https://developer.android.com/jetpack/androidx/releases/compose-kotlin // 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 import com.vitorpamplona.quartz.events.Event
class DeletionIndex { 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. // 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. // Anything newer than the date should not be deleted.
private val deletedReferencesBefore = LargeCache<DeletionRequest, Long>() private val deletedReferencesBefore = LargeCache<DeletionRequest, Long>()
fun add(event: DeletionEvent) { fun add(event: DeletionEvent): Boolean {
var atLeastOne = false
event.tags.forEach { event.tags.forEach {
if (it.size > 1 && (it[0] == "a" || it[0] == "e")) { 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( private fun add(
ref: String, ref: String,
createdAt: Long,
byPubKey: HexKey, byPubKey: HexKey,
) { createdAt: Long,
): Boolean {
val key = DeletionRequest(ref, byPubKey) val key = DeletionRequest(ref, byPubKey)
val deletionTime = deletedReferencesBefore.get(key) val previousDeletionTime = deletedReferencesBefore.get(key)
if (deletionTime == null) {
if (previousDeletionTime == null || createdAt > previousDeletionTime) {
deletedReferencesBefore.put(key, createdAt) deletedReferencesBefore.put(key, createdAt)
} else { return true
// updates with newer deletion.
if (createdAt > deletionTime) {
deletedReferencesBefore.put(key, createdAt)
}
} }
return false
} }
fun hasBeenDeleted(event: Event): Boolean { fun hasBeenDeleted(event: Event): Boolean {
@ -62,7 +76,7 @@ class DeletionIndex {
if (hasBeenDeleted(key)) return true if (hasBeenDeleted(key)) return true
if (event is AddressableEvent) { if (event is AddressableEvent) {
if (hasBeenDeleted(key, event.createdAt)) return true if (hasBeenDeleted(DeletionRequest(event.addressTag(), event.pubKey), event.createdAt)) return true
} }
return false return false
@ -75,6 +89,6 @@ class DeletionIndex {
createdAt: Long, createdAt: Long,
): Boolean { ): Boolean {
val deletionTime = deletedReferencesBefore.get(key) val deletionTime = deletedReferencesBefore.get(key)
return deletionTime != null && createdAt < deletionTime return deletionTime != null && createdAt <= deletionTime
} }
} }

Wyświetl plik

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

Wyświetl plik

@ -53,7 +53,7 @@ object Nip19Bech32 {
) )
@Immutable @Immutable
data class ParseReturn(val entity: Entity, val additionalChars: String = "") data class ParseReturn(val entity: Entity, val additionalChars: String? = null)
interface Entity interface Entity
@ -96,7 +96,7 @@ object Nip19Bech32 {
if (type == null) return null if (type == null) return null
return parseComponents(type, key, additionalChars) return parseComponents(type, key, additionalChars.ifEmpty { null })
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e) Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e)
} }
@ -123,7 +123,7 @@ object Nip19Bech32 {
"nembed1" -> nembed(bytes) "nembed1" -> nembed(bytes)
else -> null else -> null
}?.let { }?.let {
ParseReturn(it, additionalChars ?: "") ParseReturn(it, additionalChars)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e) 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 deleteAddresses() = taggedAddresses()
fun deleteAddressTags() = tags.mapNotNull { if (it.size > 1 && it[0] == "a") it[1] else null }
companion object { companion object {
const val KIND = 5 const val KIND = 5
const val ALT = "Deletion event" const val ALT = "Deletion event"

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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