kopia lustrzana https://github.com/vitorpamplona/amethyst
Porównaj commity
20 Commity
261481ecba
...
b24b37e1d4
Autor | SHA1 | Data |
---|---|---|
Tony Giorgio | b24b37e1d4 | |
Vitor Pamplona | c2f8df963a | |
Crowdin Bot | ff20960bb5 | |
Vitor Pamplona | 8dd1fc2077 | |
Vitor Pamplona | bb2fb2b103 | |
Vitor Pamplona | 00a9c49915 | |
Vitor Pamplona | d2872cc8bb | |
Vitor Pamplona | d33a1ce14f | |
Vitor Pamplona | 31958215be | |
Vitor Pamplona | bbbb614718 | |
Vitor Pamplona | 776a23c256 | |
Vitor Pamplona | 0854bd34ff | |
Vitor Pamplona | 1738a775ef | |
Vitor Pamplona | a6953872ea | |
Vitor Pamplona | d48714456c | |
Vitor Pamplona | c25aad482b | |
Crowdin Bot | cbebfd263b | |
Vitor Pamplona | 89dbe82191 | |
Vitor Pamplona | 7bb72d0c2d | |
Tony Giorgio | 08f1b43908 |
|
@ -84,7 +84,7 @@ jobs:
|
|||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
|
@ -96,7 +96,7 @@ jobs:
|
|||
id: upload-release-asset-play-universal-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-universal-release-unsigned-signed.apk
|
||||
|
@ -107,7 +107,7 @@ jobs:
|
|||
id: upload-release-asset-play-x86-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-x86-release-unsigned-signed.apk
|
||||
|
@ -118,7 +118,7 @@ jobs:
|
|||
id: upload-release-asset-play-x86-64-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-x86_64-release-unsigned-signed.apk
|
||||
|
@ -152,7 +152,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-universal-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk
|
||||
|
@ -163,7 +163,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-x86-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk
|
||||
|
@ -174,7 +174,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-x86-64-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk
|
||||
|
@ -210,7 +210,7 @@ jobs:
|
|||
id: upload-release-asset-play-aab
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/bundle/playRelease/app-play-release.aab
|
||||
|
@ -222,7 +222,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-aab
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab
|
||||
|
|
|
@ -12,8 +12,8 @@ android {
|
|||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 364
|
||||
versionName "0.86.1"
|
||||
versionCode 365
|
||||
versionName "0.86.2"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"a704a11334ed4fe6fc6ee6f8856f6f005da33644770616f1437f8b2b488b52b1\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
|
|
@ -47,9 +47,11 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler
|
||||
|
@ -101,12 +103,18 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
|||
onDismissRequest = { distributorPresent = true },
|
||||
title = { Text(stringResource(R.string.push_server_install_app)) },
|
||||
text = {
|
||||
Material3RichText(
|
||||
val content = stringResource(R.string.push_server_install_app_description)
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
renderer = null,
|
||||
) {
|
||||
Markdown(
|
||||
content = stringResource(R.string.push_server_install_app_description),
|
||||
)
|
||||
BasicMarkdown(astNode)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.util.Log
|
|||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.data.DeletionIndex
|
||||
import com.vitorpamplona.amethyst.commons.data.LargeCache
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
|
@ -133,6 +134,8 @@ object LocalCache {
|
|||
val channels = LargeCache<HexKey, Channel>()
|
||||
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
|
||||
|
||||
val deletionIndex = DeletionIndex()
|
||||
|
||||
fun checkGetOrCreateUser(key: String): User? {
|
||||
// checkNotInMainThread()
|
||||
|
||||
|
@ -168,6 +171,22 @@ object LocalCache {
|
|||
return channels.get(key)
|
||||
}
|
||||
|
||||
fun getNoteIfExists(event: Event): Note? {
|
||||
return if (event is AddressableEvent) {
|
||||
getAddressableNoteIfExists(event.addressTag())
|
||||
} else {
|
||||
getNoteIfExists(event.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrCreateNote(event: Event): Note {
|
||||
return if (event is AddressableEvent) {
|
||||
getOrCreateAddressableNote(event.address())
|
||||
} else {
|
||||
getOrCreateNote(event.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkGetOrCreateNote(key: String): Note? {
|
||||
checkNotInMainThread()
|
||||
|
||||
|
@ -956,52 +975,53 @@ object LocalCache {
|
|||
}
|
||||
|
||||
fun consume(event: DeletionEvent) {
|
||||
var deletedAtLeastOne = false
|
||||
if (deletionIndex.add(event)) {
|
||||
var deletedAtLeastOne = false
|
||||
|
||||
event
|
||||
.deleteEvents()
|
||||
.mapNotNull { getNoteIfExists(it) }
|
||||
.forEach { deleteNote ->
|
||||
// must be the same author
|
||||
if (deleteNote.author?.pubkeyHex == event.pubKey) {
|
||||
// reverts the add
|
||||
deleteNote(deleteNote)
|
||||
event.deleteEvents()
|
||||
.mapNotNull { getNoteIfExists(it) }
|
||||
.forEach { deleteNote ->
|
||||
// must be the same author
|
||||
if (deleteNote.author?.pubkeyHex == event.pubKey) {
|
||||
// reverts the add
|
||||
deleteNote(deleteNote)
|
||||
|
||||
deletedAtLeastOne = true
|
||||
deletedAtLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
val addressList = event.deleteAddressTags()
|
||||
val addressSet = addressList.toSet()
|
||||
|
||||
addressList
|
||||
.mapNotNull { getAddressableNoteIfExists(it) }
|
||||
.forEach { deleteNote ->
|
||||
// must be the same author
|
||||
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) <= event.createdAt) {
|
||||
// Counts the replies
|
||||
deleteNote(deleteNote)
|
||||
|
||||
addressables.remove(deleteNote.idHex)
|
||||
|
||||
deletedAtLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
notes.forEach { key, note ->
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is AddressableEvent && noteEvent.addressTag() in addressSet) {
|
||||
if (noteEvent.pubKey() == event.pubKey && noteEvent.createdAt() <= event.createdAt) {
|
||||
deleteNote(note)
|
||||
deletedAtLeastOne = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val addressList = event.deleteAddresses()
|
||||
val addressSet = addressList.toSet()
|
||||
|
||||
addressList
|
||||
.mapNotNull { getAddressableNoteIfExists(it.toTag()) }
|
||||
.forEach { deleteNote ->
|
||||
// must be the same author
|
||||
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) < event.createdAt) {
|
||||
// Counts the replies
|
||||
deleteNote(deleteNote)
|
||||
|
||||
addressables.remove(deleteNote.idHex)
|
||||
|
||||
deletedAtLeastOne = true
|
||||
}
|
||||
if (deletedAtLeastOne) {
|
||||
val note = Note(event.id)
|
||||
note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList())
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
notes.forEach { key, note ->
|
||||
val noteEvent = note.event
|
||||
if (noteEvent is AddressableEvent && noteEvent.address() in addressSet) {
|
||||
if (noteEvent.pubKey() == event.pubKey && noteEvent.createdAt() <= event.createdAt) {
|
||||
deleteNote(note)
|
||||
deletedAtLeastOne = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedAtLeastOne) {
|
||||
val note = Note(event.id)
|
||||
note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList())
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2210,6 +2230,8 @@ object LocalCache {
|
|||
event: Event,
|
||||
relay: Relay?,
|
||||
) {
|
||||
if (deletionIndex.hasBeenDeleted(event)) return
|
||||
|
||||
checkNotInMainThread()
|
||||
|
||||
try {
|
||||
|
|
|
@ -146,9 +146,9 @@ class NewMessageTagger(
|
|||
|
||||
fun getNostrAddress(
|
||||
bechAddress: String,
|
||||
restOfTheWord: String,
|
||||
restOfTheWord: String?,
|
||||
): String {
|
||||
return if (restOfTheWord.isEmpty()) {
|
||||
return if (restOfTheWord.isNullOrEmpty()) {
|
||||
"nostr:$bechAddress"
|
||||
} else {
|
||||
if (Bech32.ALPHABET.contains(restOfTheWord.get(0), true)) {
|
||||
|
@ -159,7 +159,7 @@ class NewMessageTagger(
|
|||
}
|
||||
}
|
||||
|
||||
@Immutable data class DirtyKeyInfo(val key: Nip19Bech32.ParseReturn, val restOfWord: String)
|
||||
@Immutable data class DirtyKeyInfo(val key: Nip19Bech32.ParseReturn, val restOfWord: String?)
|
||||
|
||||
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
|
||||
var key = mightBeAKey
|
||||
|
@ -181,7 +181,7 @@ class NewMessageTagger(
|
|||
val pubkey =
|
||||
Nip19Bech32.uriToRoute(KeyPair(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
|
||||
|
||||
return DirtyKeyInfo(pubkey, restOfWord)
|
||||
return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null })
|
||||
} else if (key.startsWith("npub1", true)) {
|
||||
if (key.length < 63) {
|
||||
return null
|
||||
|
@ -192,7 +192,7 @@ class NewMessageTagger(
|
|||
|
||||
val pubkey = Nip19Bech32.uriToRoute(keyB32) ?: return null
|
||||
|
||||
return DirtyKeyInfo(pubkey, restOfWord)
|
||||
return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null })
|
||||
} else if (key.startsWith("note1", true)) {
|
||||
if (key.length < 63) {
|
||||
return null
|
||||
|
@ -203,7 +203,7 @@ class NewMessageTagger(
|
|||
|
||||
val noteId = Nip19Bech32.uriToRoute(keyB32) ?: return null
|
||||
|
||||
return DirtyKeyInfo(noteId, restOfWord)
|
||||
return DirtyKeyInfo(noteId, restOfWord.ifEmpty { null })
|
||||
} else if (key.startsWith("nprofile", true)) {
|
||||
val pubkeyRelay = Nip19Bech32.uriToRoute(key) ?: return null
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ fun LoadOrCreateNote(
|
|||
@Composable
|
||||
private fun LoadAndDisplayEvent(
|
||||
event: Event,
|
||||
additionalChars: String,
|
||||
additionalChars: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
|
@ -141,7 +141,7 @@ private fun LoadAndDisplayEvent(
|
|||
private fun DisplayEvent(
|
||||
hex: HexKey,
|
||||
kind: Int?,
|
||||
additionalChars: String,
|
||||
additionalChars: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
|
@ -164,7 +164,7 @@ private fun DisplayNoteLink(
|
|||
it: Note,
|
||||
hex: HexKey,
|
||||
kind: Int?,
|
||||
addedCharts: String,
|
||||
addedCharts: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
|
@ -218,7 +218,7 @@ private fun DisplayNoteLink(
|
|||
@Composable
|
||||
private fun DisplayAddress(
|
||||
nip19: Nip19Bech32.NAddress,
|
||||
additionalChars: String,
|
||||
additionalChars: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
|
@ -245,16 +245,22 @@ private fun DisplayAddress(
|
|||
}
|
||||
|
||||
if (noteBase == null) {
|
||||
Text(
|
||||
remember { "@${nip19.atag}$additionalChars" },
|
||||
)
|
||||
if (additionalChars != null) {
|
||||
Text(
|
||||
remember { "@${nip19.atag}$additionalChars" },
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
remember { "@${nip19.atag}" },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayUser(
|
||||
public fun DisplayUser(
|
||||
userHex: HexKey,
|
||||
additionalChars: String,
|
||||
additionalChars: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
|
@ -274,30 +280,34 @@ private fun DisplayUser(
|
|||
userBase?.let { RenderUserAsClickableText(it, additionalChars, nav) }
|
||||
|
||||
if (userBase == null) {
|
||||
Text(
|
||||
remember { "@${userHex}$additionalChars" },
|
||||
)
|
||||
if (additionalChars != null) {
|
||||
Text(
|
||||
remember { "@${userHex}$additionalChars" },
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
remember { "@$userHex" },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderUserAsClickableText(
|
||||
baseUser: User,
|
||||
additionalChars: String,
|
||||
additionalChars: String?,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val userState by baseUser.live().userMetadataInfo.observeAsState()
|
||||
|
||||
userState?.bestName()?.let {
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = it,
|
||||
suffix = additionalChars.ifBlank { null },
|
||||
maxLines = 1,
|
||||
route = "User/${baseUser.pubkeyHex}",
|
||||
nav = nav,
|
||||
tags = userState?.tags ?: EmptyTagList,
|
||||
)
|
||||
}
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = userState?.bestName() ?: ("@" + baseUser.pubkeyDisplayHex()),
|
||||
suffix = additionalChars?.ifBlank { null },
|
||||
maxLines = 1,
|
||||
route = "User/${baseUser.pubkeyHex}",
|
||||
nav = nav,
|
||||
tags = userState?.tags ?: EmptyTagList,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -68,15 +68,16 @@ fun ExpandableRichTextViewer(
|
|||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
var showFullText by remember {
|
||||
val cached = ShowFullTextCache.cache[id]
|
||||
if (cached == null) {
|
||||
ShowFullTextCache.cache.put(id, false)
|
||||
mutableStateOf(false)
|
||||
} else {
|
||||
mutableStateOf(cached)
|
||||
var showFullText by
|
||||
remember {
|
||||
val cached = ShowFullTextCache.cache[id]
|
||||
if (cached == null) {
|
||||
ShowFullTextCache.cache.put(id, false)
|
||||
mutableStateOf(false)
|
||||
} else {
|
||||
mutableStateOf(cached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val whereToCut = remember(content) { ExpandableTextCutOffCalculator.indexToCutOff(content) }
|
||||
|
||||
|
|
|
@ -61,25 +61,7 @@ fun LoadUrlPreview(
|
|||
) { state ->
|
||||
when (state) {
|
||||
is UrlPreviewState.Loaded -> {
|
||||
if (state.previewInfo.mimeType.type == "image") {
|
||||
Box(modifier = HalfVertPadding) {
|
||||
ZoomableContentView(
|
||||
content = MediaUrlImage(url),
|
||||
roundedCorner = true,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
} else if (state.previewInfo.mimeType.type == "video") {
|
||||
Box(modifier = HalfVertPadding) {
|
||||
ZoomableContentView(
|
||||
content = MediaUrlVideo(url),
|
||||
roundedCorner = true,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
UrlPreviewCard(url, state.previewInfo)
|
||||
}
|
||||
RenderLoaded(state, url, accountViewModel)
|
||||
}
|
||||
else -> {
|
||||
ClickableUrl(urlText, url)
|
||||
|
@ -88,3 +70,30 @@ fun LoadUrlPreview(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderLoaded(
|
||||
state: UrlPreviewState.Loaded,
|
||||
url: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
if (state.previewInfo.mimeType.type == "image") {
|
||||
Box(modifier = HalfVertPadding) {
|
||||
ZoomableContentView(
|
||||
content = MediaUrlImage(url),
|
||||
roundedCorner = true,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
} else if (state.previewInfo.mimeType.type == "video") {
|
||||
Box(modifier = HalfVertPadding) {
|
||||
ZoomableContentView(
|
||||
content = MediaUrlVideo(url),
|
||||
roundedCorner = true,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
UrlPreviewCard(url, state.previewInfo)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -29,12 +29,12 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
|
@ -51,9 +51,6 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextMeasurer
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
|
@ -65,9 +62,6 @@ import androidx.compose.ui.unit.LayoutDirection
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.markdown.MarkdownParseOptions
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BechSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.CashuSegment
|
||||
|
@ -79,10 +73,8 @@ import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
|
|||
import com.vitorpamplona.amethyst.commons.richtext.ImageSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.InvoiceSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.LinkSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.PhoneSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.Segment
|
||||
|
@ -93,33 +85,29 @@ import com.vitorpamplona.amethyst.model.Note
|
|||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
||||
import com.vitorpamplona.amethyst.service.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
|
||||
import com.vitorpamplona.amethyst.ui.theme.inlinePlaceholder
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.markdownStyle
|
||||
import com.vitorpamplona.amethyst.ui.uriToRoute
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun isMarkdown(content: String): Boolean {
|
||||
return content.startsWith("> ") ||
|
||||
content.startsWith("# ") ||
|
||||
content.contains("##") ||
|
||||
content.contains("__") ||
|
||||
content.contains("**") ||
|
||||
content.contains("```") ||
|
||||
content.contains("](")
|
||||
}
|
||||
|
@ -137,7 +125,7 @@ fun RichTextViewer(
|
|||
) {
|
||||
Column(modifier = modifier) {
|
||||
if (remember(content) { isMarkdown(content) }) {
|
||||
RenderContentAsMarkdown(content, tags, accountViewModel, nav)
|
||||
RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
} else {
|
||||
RenderRegular(content, tags, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
|
@ -346,17 +334,6 @@ fun RenderRegular(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// UrlPreviews and Images have a 5dp spacing down. This also adds the space to Text.
|
||||
val lastElement = state.paragraphs.lastOrNull()?.words?.lastOrNull()
|
||||
if (lastElement !is ImageSegment &&
|
||||
lastElement !is LinkSegment &&
|
||||
lastElement !is InvoiceSegment &&
|
||||
lastElement !is CashuSegment
|
||||
) {
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -462,186 +439,6 @@ fun RenderCustomEmoji(
|
|||
)
|
||||
}
|
||||
|
||||
val markdownParseOptions =
|
||||
MarkdownParseOptions(
|
||||
autolink = true,
|
||||
isImage = { url -> RichTextParser.isImageOrVideoUrl(url) },
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun RenderContentAsMarkdown(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val uri = LocalUriHandler.current
|
||||
val onClick =
|
||||
remember {
|
||||
{ link: String ->
|
||||
val route = uriToRoute(link)
|
||||
if (route != null) {
|
||||
nav(route)
|
||||
} else {
|
||||
runCatching { uri.openUri(link) }
|
||||
}
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
ProvideTextStyle(MarkdownTextStyle) {
|
||||
Material3RichText(style = MaterialTheme.colorScheme.markdownStyle) {
|
||||
RefreshableContent(content, tags, accountViewModel) {
|
||||
Markdown(
|
||||
content = it,
|
||||
markdownParseOptions = markdownParseOptions,
|
||||
onLinkClicked = onClick,
|
||||
onMediaCompose = { title, destination ->
|
||||
ZoomableContentView(
|
||||
content =
|
||||
remember(destination, tags) {
|
||||
RichTextParser().parseMediaUrl(
|
||||
destination,
|
||||
tags ?: EmptyTagList,
|
||||
title.ifEmpty { null } ?: content,
|
||||
) ?: MediaUrlImage(url = destination, description = title.ifEmpty { null } ?: content)
|
||||
},
|
||||
roundedCorner = true,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RefreshableContent(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
accountViewModel: AccountViewModel,
|
||||
onCompose: @Composable (String) -> Unit,
|
||||
) {
|
||||
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(content) }
|
||||
|
||||
ObserverAllNIP19References(content, tags, accountViewModel) {
|
||||
accountViewModel.returnMarkdownWithSpecialContent(content, tags) { newMarkdownWithSpecialContent ->
|
||||
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
|
||||
markdownWithSpecialContent = newMarkdownWithSpecialContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
markdownWithSpecialContent?.let { onCompose(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserverAllNIP19References(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
accountViewModel: AccountViewModel,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
var nip19References by remember(content) { mutableStateOf<List<Nip19Bech32.Entity>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(key1 = content) {
|
||||
accountViewModel.returnNIP19References(content, tags) {
|
||||
nip19References = it
|
||||
onRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
nip19References.forEach { ObserveNIP19(it, accountViewModel, onRefresh) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveNIP19(
|
||||
entity: Nip19Bech32.Entity,
|
||||
accountViewModel: AccountViewModel,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
when (entity) {
|
||||
is Nip19Bech32.NPub -> ObserveNIP19User(entity.hex, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.NProfile -> ObserveNIP19User(entity.hex, accountViewModel, onRefresh)
|
||||
|
||||
is Nip19Bech32.Note -> ObserveNIP19Event(entity.hex, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.NEvent -> ObserveNIP19Event(entity.hex, accountViewModel, onRefresh)
|
||||
is Nip19Bech32.NEmbed -> ObserveNIP19Event(entity.event.id, accountViewModel, onRefresh)
|
||||
|
||||
is Nip19Bech32.NAddress -> ObserveNIP19Event(entity.atag, accountViewModel, onRefresh)
|
||||
|
||||
is Nip19Bech32.NSec -> {}
|
||||
is Nip19Bech32.NRelay -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ObserveNIP19Event(
|
||||
hex: HexKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
var baseNote by remember(hex) { mutableStateOf<Note?>(accountViewModel.getNoteIfExists(hex)) }
|
||||
|
||||
if (baseNote == null) {
|
||||
LaunchedEffect(key1 = hex) {
|
||||
accountViewModel.checkGetOrCreateNote(hex) { note ->
|
||||
launch(Dispatchers.Main) { baseNote = note }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseNote?.let { note -> ObserveNote(note, onRefresh) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveNote(
|
||||
note: Note,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
val loadedNoteId by note.live().metadata.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = loadedNoteId) {
|
||||
if (loadedNoteId != null) {
|
||||
onRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ObserveNIP19User(
|
||||
hex: HexKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
var baseUser by remember(hex) { mutableStateOf<User?>(accountViewModel.getUserIfExists(hex)) }
|
||||
|
||||
if (baseUser == null) {
|
||||
LaunchedEffect(key1 = hex) {
|
||||
accountViewModel.checkGetOrCreateUser(hex)?.let { user ->
|
||||
launch(Dispatchers.Main) { baseUser = user }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseUser?.let { user -> ObserveUser(user, onRefresh) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ObserveUser(
|
||||
user: User,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
val loadedUserMetaId by user.live().metadata.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = loadedUserMetaId) {
|
||||
if (loadedUserMetaId != null) {
|
||||
onRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BechLink(
|
||||
word: String,
|
||||
|
@ -683,7 +480,7 @@ fun BechLink(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayFullNote(
|
||||
fun DisplayFullNote(
|
||||
note: Note,
|
||||
extraChars: String?,
|
||||
quotesLeft: Int,
|
||||
|
@ -752,13 +549,7 @@ fun HashTag(
|
|||
|
||||
@Composable
|
||||
private fun InlineIcon(hashtagIcon: HashtagIcon) =
|
||||
InlineTextContent(
|
||||
Placeholder(
|
||||
width = Font17SP,
|
||||
height = Font17SP,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
|
||||
),
|
||||
) {
|
||||
InlineTextContent(inlinePlaceholder) {
|
||||
Icon(
|
||||
imageVector = hashtagIcon.icon,
|
||||
contentDescription = hashtagIcon.description,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -363,7 +363,7 @@ fun NoteMaster(
|
|||
reports,
|
||||
note.author?.let { account.isHidden(it) } ?: false,
|
||||
accountViewModel,
|
||||
Modifier,
|
||||
Modifier.fillMaxWidth(),
|
||||
nav,
|
||||
onClick = { showHiddenNote = true },
|
||||
)
|
||||
|
|
|
@ -48,6 +48,7 @@ import androidx.compose.material3.ButtonDefaults
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
|
@ -82,9 +83,11 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
|
@ -140,12 +143,18 @@ fun AccountBackupDialog(
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Material3RichText(
|
||||
val content1 = stringResource(R.string.account_backup_tips2_md)
|
||||
|
||||
val astNode1 =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
renderer = null,
|
||||
) {
|
||||
Markdown(
|
||||
content = stringResource(R.string.account_backup_tips2_md),
|
||||
)
|
||||
BasicMarkdown(astNode1)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
@ -154,12 +163,18 @@ fun AccountBackupDialog(
|
|||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
Material3RichText(
|
||||
val content = stringResource(R.string.account_backup_tips3_md)
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = RichTextStyle().resolveDefaults(),
|
||||
renderer = null,
|
||||
) {
|
||||
Markdown(
|
||||
content = stringResource(R.string.account_backup_tips3_md),
|
||||
)
|
||||
BasicMarkdown(astNode)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
@ -312,7 +327,7 @@ private fun EncryptNSecCopyButton(
|
|||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
OutlinedButton(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
authenticate(
|
||||
|
@ -324,15 +339,10 @@ private fun EncryptNSecCopyButton(
|
|||
)
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
contentPadding = ButtonPadding,
|
||||
enabled = password.value.text.isNotBlank(),
|
||||
) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
imageVector = Icons.Default.Key,
|
||||
contentDescription =
|
||||
stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup),
|
||||
|
@ -340,7 +350,6 @@ private fun EncryptNSecCopyButton(
|
|||
)
|
||||
Text(
|
||||
stringResource(id = R.string.encrypt_and_copy_my_secret_key),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,6 @@ import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
|||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.actions.Dao
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||
import com.vitorpamplona.amethyst.ui.components.MarkdownParser
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottomNavigationItems
|
||||
|
@ -79,7 +78,6 @@ import com.vitorpamplona.quartz.events.DraftEvent
|
|||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.events.Participant
|
||||
|
@ -1015,33 +1013,6 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||
}
|
||||
}
|
||||
|
||||
fun returnNIP19References(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
onNewReferences: (List<Nip19Bech32.Entity>) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onNewReferences(MarkdownParser().returnNIP19References(content, tags))
|
||||
}
|
||||
}
|
||||
|
||||
fun returnMarkdownWithSpecialContent(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
onNewContent: (String) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onNewContent(MarkdownParser().returnMarkdownWithSpecialContent(content, tags))
|
||||
}
|
||||
}
|
||||
|
||||
fun loadNEmbedIfNeeded(nembed: Event) {
|
||||
val baseNote = LocalCache.getNoteIfExists(nembed.id)
|
||||
if (baseNote?.event == null) {
|
||||
LocalCache.verifyAndConsume(nembed, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkIsOnline(
|
||||
media: String?,
|
||||
onDone: (Boolean) -> Unit,
|
||||
|
@ -1374,11 +1345,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||
is Nip19Bech32.NEvent -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } }
|
||||
is Nip19Bech32.NEmbed ->
|
||||
withContext(Dispatchers.IO) {
|
||||
accountViewModel.loadNEmbedIfNeeded(parsed.event)
|
||||
|
||||
LocalCache.checkGetOrCreateNote(parsed.event.id)?.let { note ->
|
||||
returningNote = note
|
||||
val baseNote = LocalCache.getOrCreateNote(parsed.event)
|
||||
if (baseNote.event == null) {
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.verifyAndConsume(parsed.event, null)
|
||||
}
|
||||
}
|
||||
|
||||
returningNote = baseNote
|
||||
}
|
||||
is Nip19Bech32.NRelay -> {}
|
||||
is Nip19Bech32.NAddress -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.atag)?.let { note -> returningNote = note } }
|
||||
|
|
|
@ -36,6 +36,7 @@ import androidx.compose.material3.Surface
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
@ -47,8 +48,10 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.ui.material3.Material3RichText
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
|
@ -112,12 +115,18 @@ fun ConnectOrbotDialog(
|
|||
)
|
||||
|
||||
Row {
|
||||
Material3RichText(
|
||||
val content1 = stringResource(R.string.connect_through_your_orbot_setup_markdown)
|
||||
|
||||
val astNode1 =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = myMarkDownStyle,
|
||||
renderer = null,
|
||||
) {
|
||||
Markdown(
|
||||
content = stringResource(R.string.connect_through_your_orbot_setup_markdown),
|
||||
)
|
||||
BasicMarkdown(astNode1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,8 @@ import androidx.compose.material3.Shapes
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Shapes =
|
||||
|
@ -231,3 +233,10 @@ val liveStreamTag =
|
|||
val chatAuthorBox = Modifier.size(20.dp)
|
||||
val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape)
|
||||
val AuthorInfoVideoFeed = Modifier.width(75.dp).padding(end = 15.dp)
|
||||
|
||||
val inlinePlaceholder =
|
||||
Placeholder(
|
||||
width = Font17SP,
|
||||
height = Font17SP,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
|
||||
)
|
||||
|
|
|
@ -59,7 +59,7 @@ val Font18SP = 18.sp
|
|||
|
||||
val MarkdownTextStyle = TextStyle(lineHeight = 1.30.em)
|
||||
|
||||
val DefaultParagraphSpacing: TextUnit = 16.sp
|
||||
val DefaultParagraphSpacing: TextUnit = 18.sp
|
||||
|
||||
internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->
|
||||
when (level) {
|
||||
|
|
|
@ -276,6 +276,7 @@
|
|||
<string name="report_dialog_title">Bloquear y reportar</string>
|
||||
<string name="block_only">Bloquear</string>
|
||||
<string name="bookmarks">Marcadores</string>
|
||||
<string name="drafts">Borradores</string>
|
||||
<string name="private_bookmarks">Marcadores privados</string>
|
||||
<string name="public_bookmarks">Marcadores públicos</string>
|
||||
<string name="add_to_private_bookmarks">Agregar a marcadores privados</string>
|
||||
|
|
|
@ -275,6 +275,7 @@
|
|||
<string name="report_dialog_title">Tiltás és Jelentés</string>
|
||||
<string name="block_only">Tiltás</string>
|
||||
<string name="bookmarks">Könyvjelzők</string>
|
||||
<string name="drafts">Piszkozatok</string>
|
||||
<string name="private_bookmarks">Privát Könyvjelzők</string>
|
||||
<string name="public_bookmarks">Publikus Könyvjelzők</string>
|
||||
<string name="add_to_private_bookmarks">Hozzáadás a Privát Könyvjelzőimhez</string>
|
||||
|
@ -621,6 +622,7 @@
|
|||
<string name="server_did_not_provide_a_url_after_uploading">A szerver a feltöltés után nem adott URL-t</string>
|
||||
<string name="could_not_download_from_the_server">Nem sikerült a szerverről a feltöltött médiát letölteni</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Nem sikerült előkészíteni a helyi fájlt a feltöltésre: %1$s</string>
|
||||
<string name="edit_draft">Piszkozat szerkesztése</string>
|
||||
<string name="login_with_qr_code">Bejelentkezés QR kóddal</string>
|
||||
<string name="route">Útvonal</string>
|
||||
<string name="route_home">Főoldal</string>
|
||||
|
@ -692,4 +694,10 @@
|
|||
<string name="accessibility_play_username">Felhasználónév lejátszása hangként</string>
|
||||
<string name="accessibility_scan_qr_code">QR-kód beolvasása</string>
|
||||
<string name="accessibility_navigate_to_alby">Keresd fel a harmadik féltől származó pénztárcaszolgáltatót, az Alby-t</string>
|
||||
<string name="it_s_not_possible_to_reply_to_a_draft_note">A bejegyzésvázlatra nem lehet válaszolni</string>
|
||||
<string name="it_s_not_possible_to_quote_to_a_draft_note">A bejegyzésvázlatot nem lehet idézni</string>
|
||||
<string name="it_s_not_possible_to_react_to_a_draft_note">A bejegyzésvázlatra nem lehet reagálni</string>
|
||||
<string name="it_s_not_possible_to_zap_to_a_draft_note">A bejegyzésvázlatra nem lehet zap-pelni</string>
|
||||
<string name="draft_note">Bejegyzéspiszkozat</string>
|
||||
<string name="load_from_text">Tőle üzenet</string>
|
||||
</resources>
|
||||
|
|
|
@ -700,4 +700,5 @@
|
|||
<string name="it_s_not_possible_to_react_to_a_draft_note">Het is niet mogelijk om op een concept te reageren</string>
|
||||
<string name="it_s_not_possible_to_zap_to_a_draft_note">Het is niet mogelijk om een concept op te zappen</string>
|
||||
<string name="draft_note">Concept notitie</string>
|
||||
<string name="load_from_text">Van Msg</string>
|
||||
</resources>
|
||||
|
|
|
@ -275,6 +275,7 @@
|
|||
<string name="report_dialog_title">บล๊อกและรายงาน</string>
|
||||
<string name="block_only">บล๊อก</string>
|
||||
<string name="bookmarks">บุ๊คมาร์ค</string>
|
||||
<string name="drafts">ฉบับร่าง</string>
|
||||
<string name="private_bookmarks">บุ๊คมาร์คส่วนตัว</string>
|
||||
<string name="public_bookmarks">บุ๊คมาร์คสาธรณะ</string>
|
||||
<string name="add_to_private_bookmarks">เพิ่มไปยังบุ๊คมาร์คส่วนตัว</string>
|
||||
|
@ -440,6 +441,8 @@
|
|||
<string name="connectivity_type_always">ตลอดเวลา</string>
|
||||
<string name="connectivity_type_wifi_only">Wifi เท่านั้น</string>
|
||||
<string name="connectivity_type_never">ไม่ต้องแสดง</string>
|
||||
<string name="ui_feature_set_type_complete">เสร็จสมบูรณ์</string>
|
||||
<string name="ui_feature_set_type_simplified">Simplified</string>
|
||||
<string name="system">ระบบ(ค่าพื้นฐาน)</string>
|
||||
<string name="light">สว่าง</string>
|
||||
<string name="dark">มืด</string>
|
||||
|
@ -451,6 +454,8 @@
|
|||
<string name="automatically_show_url_preview">การแสดงตัวอย่าง URL</string>
|
||||
<string name="automatically_hide_nav_bars">การไถฟีดแบบลื่นไหล</string>
|
||||
<string name="automatically_hide_nav_bars_description">ซ่อนแถบเมนูขณะเลื่อนฟีด</string>
|
||||
<string name="ui_style">UI Mode</string>
|
||||
<string name="ui_style_description">เลือกรูปแบบการโพสต์</string>
|
||||
<string name="load_image">โหลดรูปภาพ</string>
|
||||
<string name="spamming_users">แสปม</string>
|
||||
<string name="muted_button">ปิดการมองเห็น คลิกเพื่อปลดออก</string>
|
||||
|
@ -615,6 +620,7 @@
|
|||
<string name="server_did_not_provide_a_url_after_uploading">เซอเวอร์ไม่ตอบสนองหลังจากอัพโหลดแล้ว</string>
|
||||
<string name="could_not_download_from_the_server">ไม่สามารถโหลดข้อมูลสื่อได้</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">ไม่สามารถเตรียมข้อมูลสำหรับอัพโหลด : %1$s</string>
|
||||
<string name="edit_draft">แก้ไขฉบับร่าง</string>
|
||||
<string name="login_with_qr_code">เข้าสู่ระบบด้วย qr code</string>
|
||||
<string name="route">เส้นทาง</string>
|
||||
<string name="route_home">หน้าแรก</string>
|
||||
|
@ -638,11 +644,58 @@
|
|||
<string name="relay_info">รีเลย์ %1$s</string>
|
||||
<string name="expand_relay_list">แสดงรายการรีเลย์</string>
|
||||
<string name="note_options">ตัวเลือกเพิ่มเติม</string>
|
||||
<string name="relay_list_selector">ตัวเลือกรายการรีเลย์</string>
|
||||
<string name="poll">โพลล์</string>
|
||||
<string name="disable_poll">ยกเลิกโพลล์</string>
|
||||
<string name="add_bitcoin_invoice">Bitcoin Invoice</string>
|
||||
<string name="cancel_bitcoin_invoice">ยกเลิก Bitcoin Invoice</string>
|
||||
<string name="cancel_classifieds">ยกเลิกการขายสิ่งนี้</string>
|
||||
<string name="add_zapraiser">ระดมทุน</string>
|
||||
<string name="cancel_zapraiser">ยกเลิกการระดมทุน</string>
|
||||
<string name="add_location">ตำแหน่ง</string>
|
||||
<string name="remove_location">ลบตำแหน่งออก</string>
|
||||
<string name="add_zap_split">Zap splits</string>
|
||||
<string name="cancel_zap_split">ยกเลิก Zap split</string>
|
||||
<string name="add_content_warning">เพิ่มคําเตือนเนื้อหา</string>
|
||||
<string name="remove_content_warning">ลบคําเตือนเนื้อหา</string>
|
||||
<string name="show_npub_as_a_qr_code">แสดง npub เป็น qr code</string>
|
||||
<string name="invalid_nip19_uri">ที่อยู่ไม่ถูกต้อง</string>
|
||||
<string name="invalid_nip19_uri_description">Amethyst ได้รับ URI แต่ URI นั้นไม่ถูกต้อง: %1$s</string>
|
||||
<string name="zap_the_devs_title">zap ให้นักพัฒนา</string>
|
||||
<string name="zap_the_devs_description">การบริจาคของคุณช่วยให้เราสร้างความแตกต่าง ทุก sat มีค่า!</string>
|
||||
<string name="donate_now">บริจาค</string>
|
||||
<string name="brought_to_you_by">มาถึงคุณโดย:</string>
|
||||
<string name="this_version_brought_to_you_by">เวอร์ชันนี้นำเสนอโดย:</string>
|
||||
<string name="version_name">เวอร์ชัน %1$s</string>
|
||||
<string name="thank_you">ขอบคุณ!</string>
|
||||
<string name="max_limit">Max Limit</string>
|
||||
<string name="restricted_writes">การเขียนที่ถูกจำกัด</string>
|
||||
<string name="forked_from">คัดลอกมาจาก</string>
|
||||
<string name="forked_tag">คัดลอก</string>
|
||||
<string name="git_repository">Git Repository: %1$s</string>
|
||||
<string name="git_web_address">เว็บไซน์</string>
|
||||
<string name="git_clone_address">สำเนา:</string>
|
||||
<string name="existed_since">OTS: %1$s</string>
|
||||
<string name="ots_info_title">Timestamp ถูกยืนยัน</string>
|
||||
<string name="ots_info_description">มีหลักฐานว่าโพสต์นี้มีการลงนามก่อน %1$s หลักฐานถูกประทับตราใน Bitcoin blockchain ณ วันและเวลาดังกล่าว</string>
|
||||
<string name="edit_post">แก้ไขโพสต์</string>
|
||||
<string name="proposal_to_edit">เสนอให้ปรับปรุงการโพสต์</string>
|
||||
<string name="message_to_author">สรุปการเปลี่ยนแปลง</string>
|
||||
<string name="message_to_author_placeholder">แก้ไขด่วน</string>
|
||||
<string name="accept_the_suggestion">ยอมรับการแนะนำนี้</string>
|
||||
<string name="accessibility_download_for_offline">ดาวน์โหลด</string>
|
||||
<string name="accessibility_lyrics_on">Lyrics on</string>
|
||||
<string name="accessibility_lyrics_off">Lyrics off</string>
|
||||
<string name="accessibility_turn_on_sealed_message">ปิดผนึกข้อความแล้ว คลิกเพื่อเปิดข้อความ</string>
|
||||
<string name="accessibility_turn_off_sealed_message">เปิดผนึกข้อความไว้ คลิกเพื่อปิด</string>
|
||||
<string name="accessibility_send">ส่ง</string>
|
||||
<string name="accessibility_play_username">อ่านชื่อผู้ใช้เป็นเสียง</string>
|
||||
<string name="accessibility_scan_qr_code">สแกนคิวอาร์โค้ด</string>
|
||||
<string name="accessibility_navigate_to_alby">นำทางไปยังผู้ให้บริการกระเป๋าเงินบุคคลที่สาม Alby</string>
|
||||
<string name="it_s_not_possible_to_reply_to_a_draft_note">ไม่สามารถตอบกลับโน้ตฉบับร่างได้</string>
|
||||
<string name="it_s_not_possible_to_quote_to_a_draft_note">ไม่สามารถโควทโน้ตฉบับร่างได้</string>
|
||||
<string name="it_s_not_possible_to_react_to_a_draft_note">ไม่สามารถกดรีแอคโน้ตฉบับร่างได้</string>
|
||||
<string name="it_s_not_possible_to_zap_to_a_draft_note">ไม่สามารถ zap โน้ตฉบับร่างได้</string>
|
||||
<string name="draft_note">โน๊ตฉบับร่าง</string>
|
||||
<string name="load_from_text">From Msg</string>
|
||||
</resources>
|
||||
|
|
|
@ -60,7 +60,7 @@ class NewMessageTaggerKeyParseTest {
|
|||
"1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605",
|
||||
(result?.key?.entity as? Nip19Bech32.Note)?.hex,
|
||||
)
|
||||
assertEquals("", result?.restOfWord)
|
||||
assertEquals(null, result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -73,7 +73,7 @@ class NewMessageTaggerKeyParseTest {
|
|||
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||
(result?.key?.entity as? Nip19Bech32.NPub)?.hex,
|
||||
)
|
||||
assertEquals("", result?.restOfWord)
|
||||
assertEquals(null, result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* 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.commons.data
|
||||
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
|
||||
class DeletionIndex {
|
||||
data class DeletionRequest(val reference: String, val publicKey: HexKey) : Comparable<DeletionRequest> {
|
||||
override fun compareTo(other: DeletionRequest): Int {
|
||||
val compared = reference.compareTo(other.reference)
|
||||
|
||||
return if (compared == 0) {
|
||||
publicKey.compareTo(publicKey)
|
||||
} else {
|
||||
compared
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stores a set of id OR atags (kind:pubkey:dtag) by pubkey with the created at of the deletion event.
|
||||
// Anything newer than the date should not be deleted.
|
||||
private val deletedReferencesBefore = LargeCache<DeletionRequest, Long>()
|
||||
|
||||
fun add(event: DeletionEvent): Boolean {
|
||||
var atLeastOne = false
|
||||
|
||||
event.tags.forEach {
|
||||
if (it.size > 1 && (it[0] == "a" || it[0] == "e")) {
|
||||
if (add(it[1], event.pubKey, event.createdAt)) {
|
||||
atLeastOne = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return atLeastOne
|
||||
}
|
||||
|
||||
private fun add(
|
||||
ref: String,
|
||||
byPubKey: HexKey,
|
||||
createdAt: Long,
|
||||
): Boolean {
|
||||
val key = DeletionRequest(ref, byPubKey)
|
||||
val previousDeletionTime = deletedReferencesBefore.get(key)
|
||||
|
||||
if (previousDeletionTime == null || createdAt > previousDeletionTime) {
|
||||
deletedReferencesBefore.put(key, createdAt)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun hasBeenDeleted(event: Event): Boolean {
|
||||
val key = DeletionRequest(event.id, event.pubKey)
|
||||
if (hasBeenDeleted(key)) return true
|
||||
|
||||
if (event is AddressableEvent) {
|
||||
if (hasBeenDeleted(DeletionRequest(event.addressTag(), event.pubKey), event.createdAt)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun hasBeenDeleted(key: DeletionRequest) = deletedReferencesBefore.containsKey(key)
|
||||
|
||||
private fun hasBeenDeleted(
|
||||
key: DeletionRequest,
|
||||
createdAt: Long,
|
||||
): Boolean {
|
||||
val deletionTime = deletedReferencesBefore.get(key)
|
||||
return deletionTime != null && createdAt <= deletionTime
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ kotlinxCollectionsImmutable = "0.3.7"
|
|||
languageId = "17.0.5"
|
||||
lazysodiumAndroid = "5.1.0"
|
||||
lightcompressor = "1.3.2"
|
||||
markdown = "48702a8ced"
|
||||
markdown = "077a2cde64"
|
||||
media3 = "1.3.0"
|
||||
mockk = "1.13.10"
|
||||
navigationCompose = "2.7.7"
|
||||
|
|
|
@ -25,7 +25,7 @@ import androidx.compose.runtime.Immutable
|
|||
|
||||
@Immutable
|
||||
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val relay: String?) {
|
||||
fun toTag() = "$kind:$pubKeyHex:$dTag"
|
||||
fun toTag() = assembleATag(kind, pubKeyHex, dTag)
|
||||
|
||||
fun toNAddr(): String {
|
||||
return TlvBuilder()
|
||||
|
@ -40,6 +40,12 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun assembleATag(
|
||||
kind: Int,
|
||||
pubKeyHex: String,
|
||||
dTag: String,
|
||||
) = "$kind:$pubKeyHex:$dTag"
|
||||
|
||||
fun isATag(key: String): Boolean {
|
||||
return key.startsWith("naddr1") || key.contains(":")
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ object Nip19Bech32 {
|
|||
)
|
||||
|
||||
@Immutable
|
||||
data class ParseReturn(val entity: Entity, val additionalChars: String = "")
|
||||
data class ParseReturn(val entity: Entity, val additionalChars: String? = null)
|
||||
|
||||
interface Entity
|
||||
|
||||
|
@ -96,7 +96,7 @@ object Nip19Bech32 {
|
|||
|
||||
if (type == null) return null
|
||||
|
||||
return parseComponents(type, key, additionalChars)
|
||||
return parseComponents(type, key, additionalChars.ifEmpty { null })
|
||||
} catch (e: Throwable) {
|
||||
Log.e("NIP19 Parser", "Issue trying to Decode NIP19 $uri: ${e.message}", e)
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ object Nip19Bech32 {
|
|||
"nembed1" -> nembed(bytes)
|
||||
else -> null
|
||||
}?.let {
|
||||
ParseReturn(it, additionalChars ?: "")
|
||||
ParseReturn(it, additionalChars)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e)
|
||||
|
|
|
@ -38,6 +38,8 @@ class DeletionEvent(
|
|||
|
||||
fun deleteAddresses() = taggedAddresses()
|
||||
|
||||
fun deleteAddressTags() = tags.mapNotNull { if (it.size > 1 && it[0] == "a") it[1] else null }
|
||||
|
||||
companion object {
|
||||
const val KIND = 5
|
||||
const val ALT = "Deletion event"
|
||||
|
|
|
@ -115,7 +115,7 @@ class DraftEvent(
|
|||
pubKey: HexKey,
|
||||
dTag: String,
|
||||
): String {
|
||||
return ATag(KIND, pubKey, dTag, null).toTag()
|
||||
return ATag.assembleATag(KIND, pubKey, dTag)
|
||||
}
|
||||
|
||||
fun create(
|
||||
|
|
|
@ -514,6 +514,8 @@ interface AddressableEvent {
|
|||
fun dTag(): String
|
||||
|
||||
fun address(): ATag
|
||||
|
||||
fun addressTag(): String
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
@ -529,6 +531,11 @@ open class BaseAddressableEvent(
|
|||
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||
|
||||
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||
|
||||
/**
|
||||
* Creates the tag in a memory effecient way (without creating the ATag class
|
||||
*/
|
||||
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())
|
||||
}
|
||||
|
||||
fun String.bytesUsedInMemory(): Int {
|
||||
|
|
|
@ -39,6 +39,8 @@ class LongTextNoteEvent(
|
|||
|
||||
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||
|
||||
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())
|
||||
|
||||
fun topics() = hashtags()
|
||||
|
||||
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
|
||||
|
|
|
@ -39,6 +39,8 @@ class WikiNoteEvent(
|
|||
|
||||
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||
|
||||
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())
|
||||
|
||||
fun topics() = hashtags()
|
||||
|
||||
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
|
||||
|
|
Ładowanie…
Reference in New Issue