mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-05 14:24:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
308a79a305 | ||
|
|
5e5c393472 | ||
|
|
ea905ef6ed | ||
|
|
5f98a54452 | ||
|
|
8627fd4b4c | ||
|
|
2a65b7241a | ||
|
|
7863961aba | ||
|
|
a84be7fc61 | ||
|
|
657f99a65a | ||
|
|
91591abd14 | ||
|
|
fd58da2a93 | ||
|
|
b35a59372c | ||
|
|
c1113f9df9 | ||
|
|
bd3d7e1aa3 | ||
|
|
47f3fe5cc6 | ||
|
|
b6e16ad470 | ||
|
|
fe80b509e3 |
+2
-2
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 85
|
||||
versionName "0.22.2"
|
||||
versionCode 86
|
||||
versionName "0.22.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -1,71 +1,103 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.toByteArray
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.model.ATag
|
||||
import nostr.postr.bechToBytes
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import nostr.postr.Bech32
|
||||
import nostr.postr.bechToBytes
|
||||
import nostr.postr.toByteArray
|
||||
|
||||
class Nip19 {
|
||||
|
||||
enum class Type {
|
||||
USER, NOTE, RELAY, ADDRESS
|
||||
}
|
||||
|
||||
data class Return(val type: Type, val hex: String)
|
||||
|
||||
fun uriToRoute(uri: String?): Return? {
|
||||
try {
|
||||
val key = uri?.removePrefix("nostr:")
|
||||
val key = uri?.removePrefix("nostr:") ?: return null
|
||||
|
||||
if (key != null) {
|
||||
val bytes = key.bechToBytes()
|
||||
if (key.startsWith("npub")) {
|
||||
return Return(Type.USER, bytes.toHexKey())
|
||||
}
|
||||
if (key.startsWith("note")) {
|
||||
return Return(Type.NOTE, bytes.toHexKey())
|
||||
}
|
||||
if (key.startsWith("nprofile")) {
|
||||
val tlv = parseTLV(bytes)
|
||||
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey()
|
||||
if (hex != null)
|
||||
return Return(Type.USER, hex)
|
||||
}
|
||||
if (key.startsWith("nevent")) {
|
||||
val tlv = parseTLV(bytes)
|
||||
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey()
|
||||
if (hex != null)
|
||||
return Return(Type.USER, hex)
|
||||
}
|
||||
if (key.startsWith("nrelay")) {
|
||||
val tlv = parseTLV(bytes)
|
||||
val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8)
|
||||
if (relayUrl != null)
|
||||
return Return(Type.RELAY, relayUrl)
|
||||
}
|
||||
if (key.startsWith("naddr")) {
|
||||
val tlv = parseTLV(bytes)
|
||||
val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8)
|
||||
val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8)
|
||||
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey()
|
||||
val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) }
|
||||
if (d != null)
|
||||
return Return(Type.ADDRESS, "$kind:$author:$d")
|
||||
}
|
||||
val bytes = key.bechToBytes()
|
||||
if (key.startsWith("npub")) {
|
||||
return npub(bytes)
|
||||
} else if (key.startsWith("note")) {
|
||||
return note(bytes)
|
||||
} else if (key.startsWith("nprofile")) {
|
||||
return nprofile(bytes)
|
||||
} else if (key.startsWith("nevent")) {
|
||||
return nevent(bytes)
|
||||
} else if (key.startsWith("nrelay")) {
|
||||
return nrelay(bytes)
|
||||
} else if (key.startsWith("naddr")) {
|
||||
return naddr(bytes)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
|
||||
//e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun npub(bytes: ByteArray): Return {
|
||||
return Return(Type.USER, bytes.toHexKey())
|
||||
}
|
||||
|
||||
private fun note(bytes: ByteArray): Return {
|
||||
return Return(Type.NOTE, bytes.toHexKey());
|
||||
}
|
||||
|
||||
private fun nprofile(bytes: ByteArray): Return? {
|
||||
val hex = parseTLV(bytes)
|
||||
.get(NIP19TLVTypes.SPECIAL.id)
|
||||
?.get(0)
|
||||
?.toHexKey() ?: return null
|
||||
|
||||
return Return(Type.USER, hex)
|
||||
}
|
||||
|
||||
private fun nevent(bytes: ByteArray): Return? {
|
||||
val hex = parseTLV(bytes)
|
||||
.get(NIP19TLVTypes.SPECIAL.id)
|
||||
?.get(0)
|
||||
?.toHexKey() ?: return null
|
||||
|
||||
return Return(Type.USER, hex)
|
||||
}
|
||||
|
||||
private fun nrelay(bytes: ByteArray): Return? {
|
||||
val relayUrl = parseTLV(bytes)
|
||||
.get(NIP19TLVTypes.SPECIAL.id)
|
||||
?.get(0)
|
||||
?.toString(Charsets.UTF_8) ?: return null
|
||||
|
||||
return Return(Type.RELAY, relayUrl)
|
||||
}
|
||||
|
||||
private fun naddr(bytes: ByteArray): Return? {
|
||||
val tlv = parseTLV(bytes)
|
||||
|
||||
val d = tlv.get(NIP19TLVTypes.SPECIAL.id)
|
||||
?.get(0)
|
||||
?.toString(Charsets.UTF_8) ?: return null
|
||||
|
||||
val relay = tlv.get(NIP19TLVTypes.RELAY.id)
|
||||
?.get(0)
|
||||
?.toString(Charsets.UTF_8)
|
||||
|
||||
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)
|
||||
?.get(0)
|
||||
?.toHexKey()
|
||||
|
||||
val kind = tlv.get(NIP19TLVTypes.KIND.id)
|
||||
?.get(0)
|
||||
?.let { toInt32(it) }
|
||||
|
||||
return Return(Type.ADDRESS, "$kind:$author:$d")
|
||||
}
|
||||
}
|
||||
|
||||
enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin
|
||||
// Classes should start with an uppercase letter in kotlin
|
||||
enum class NIP19TLVTypes(val id: Byte) {
|
||||
SPECIAL(0),
|
||||
RELAY(1),
|
||||
AUTHOR(2),
|
||||
@@ -78,19 +110,19 @@ fun toInt32(bytes: ByteArray): Int {
|
||||
}
|
||||
|
||||
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
|
||||
var result = mutableMapOf<Byte, MutableList<ByteArray>>()
|
||||
val result = mutableMapOf<Byte, MutableList<ByteArray>>()
|
||||
var rest = data
|
||||
while (rest.isNotEmpty()) {
|
||||
val t = rest[0]
|
||||
val l = rest[1]
|
||||
val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
|
||||
rest = rest.sliceArray(IntRange(2 + l, rest.size-1))
|
||||
rest = rest.sliceArray(IntRange(2 + l, rest.size - 1))
|
||||
if (v.size < l) continue
|
||||
|
||||
if (!result.containsKey(t)) {
|
||||
result.put(t, mutableListOf())
|
||||
result[t] = mutableListOf()
|
||||
}
|
||||
result.get(t)?.add(v)
|
||||
result[t]?.add(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class Relay(
|
||||
if (socket != null) return
|
||||
|
||||
try {
|
||||
val request = Request.Builder().header("Origin", "amethyst.social").url(url.trim()).build()
|
||||
val request = Request.Builder().url(url.trim()).build()
|
||||
val listener = object : WebSocketListener() {
|
||||
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
|
||||
+14
-1
@@ -13,6 +13,7 @@ import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -22,11 +23,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
|
||||
@Composable
|
||||
fun ExpandableRichTextViewer(
|
||||
@@ -43,7 +46,17 @@ fun ExpandableRichTextViewer(
|
||||
val text = if (showFullText) content else content.take(350)
|
||||
|
||||
Box(contentAlignment = Alignment.BottomCenter) {
|
||||
RichTextViewer(text, canPreview, modifier, tags, backgroundColor, accountViewModel, navController)
|
||||
//CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||
RichTextViewer(
|
||||
text,
|
||||
canPreview,
|
||||
modifier,
|
||||
tags,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
navController
|
||||
)
|
||||
//}
|
||||
|
||||
if (content.length > 350 && !showFullText) {
|
||||
Row(
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -132,8 +133,8 @@ fun RichTextViewer(
|
||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||
content.split('\n').forEach { paragraph ->
|
||||
FlowRow() {
|
||||
paragraph.split(' ').forEach { word: String ->
|
||||
|
||||
val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' ');
|
||||
s.forEach { word: String ->
|
||||
if (canPreview) {
|
||||
// Explicit URL
|
||||
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
||||
@@ -191,6 +192,10 @@ fun RichTextViewer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun isArabic(text: String): Boolean {
|
||||
return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' }
|
||||
}
|
||||
|
||||
|
||||
fun isBechLink(word: String): Boolean {
|
||||
return word.startsWith("nostr:", true)
|
||||
|
||||
-1
@@ -183,7 +183,6 @@ fun TranslateableRichTextViewer(
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
// TODO : Rashed translate this
|
||||
Text(
|
||||
"${stringResource(R.string.show_in)} ${Locale(source).displayName} ${
|
||||
stringResource(
|
||||
|
||||
@@ -241,7 +241,7 @@ fun NoteCompose(
|
||||
|
||||
if (noteEvent is RepostEvent) {
|
||||
Text(
|
||||
" boosted",
|
||||
" ${stringResource(id = R.string.boosted)}",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
|
||||
@@ -63,9 +63,9 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>): ViewModel() {
|
||||
val oldNotesState = feedContent.value
|
||||
if (oldNotesState is FeedState.Loaded) {
|
||||
// Using size as a proxy for has changed.
|
||||
//if (notes != oldNotesState.feed.value) {
|
||||
if (notes != oldNotesState.feed.value) {
|
||||
updateFeed(notes)
|
||||
//}
|
||||
}
|
||||
} else {
|
||||
updateFeed(notes)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>): Vie
|
||||
val oldNotesState = feedContent.value
|
||||
if (oldNotesState is LnZapFeedState.Loaded) {
|
||||
// Using size as a proxy for has changed.
|
||||
updateFeed(notes)
|
||||
if (notes != oldNotesState.feed.value) {
|
||||
updateFeed(notes)
|
||||
}
|
||||
} else {
|
||||
updateFeed(notes)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,9 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>): ViewModel() {
|
||||
val oldNotesState = feedContent.value
|
||||
if (oldNotesState is UserFeedState.Loaded) {
|
||||
// Using size as a proxy for has changed.
|
||||
updateFeed(notes)
|
||||
if (notes != oldNotesState.feed.value) {
|
||||
updateFeed(notes)
|
||||
}
|
||||
} else {
|
||||
updateFeed(notes)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">مشاهدة العد</string>
|
||||
<string name="boost">تعزيز</string>
|
||||
<string name="boosted">معزز</string>
|
||||
<string name="quote">إقتباس</string>
|
||||
<string name="new_amount_in_sats">مبلغ جديد في Sats</string>
|
||||
<string name="add">إضافة</string>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">Total vistas</string>
|
||||
<string name="boost">Impulsar</string>
|
||||
<string name="boosted">boosted</string>
|
||||
<string name="quote">Cita</string>
|
||||
<string name="new_amount_in_sats">Nueva cantidad en Sats</string>
|
||||
<string name="add">Añadir</string>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">Contagem de visualizações</string>
|
||||
<string name="boost">Impulsionar</string>
|
||||
<string name="boosted">boosted</string>
|
||||
<string name="quote">Citar</string>
|
||||
<string name="new_amount_in_sats">Novo Valor em Sats</string>
|
||||
<string name="add">Adicionar</string>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<string name="zaps">Запы</string>
|
||||
<string name="view_count">Просмотры</string>
|
||||
<string name="boost">Продвинуть</string>
|
||||
<string name="boosted">boosted</string>
|
||||
<string name="quote">Цитировать</string>
|
||||
<string name="new_amount_in_sats">Новая сумма в sat</string>
|
||||
<string name="add">Добавить</string>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<string name="zaps">Запи</string>
|
||||
<string name="view_count">Перегляди</string>
|
||||
<string name="boost">Просувати</string>
|
||||
<string name="boosted">boosted</string>
|
||||
<string name="quote">Цитувати</string>
|
||||
<string name="new_amount_in_sats">Нова сума в sat</string>
|
||||
<string name="add">Додати</string>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">View count</string>
|
||||
<string name="boost">Boost</string>
|
||||
<string name="boosted">boosted</string>
|
||||
<string name="quote">Quote</string>
|
||||
<string name="new_amount_in_sats">New Amount in Sats</string>
|
||||
<string name="add">Add</string>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
class Nip19Test {
|
||||
|
||||
private val nip19 = Nip19();
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun to_int_32_length_smaller_than_4() {
|
||||
toInt32(byteArrayOfInts(1, 2, 3))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun to_int_32_length_bigger_than_4() {
|
||||
toInt32(byteArrayOfInts(1, 2, 3, 4, 5))
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun to_int_32_length_4() {
|
||||
val actual = toInt32(byteArrayOfInts(1, 2, 3, 4))
|
||||
|
||||
Assert.assertEquals(16909060, actual)
|
||||
}
|
||||
|
||||
@Ignore("Test not implemented yet")
|
||||
@Test()
|
||||
fun parse_TLV() {
|
||||
// TODO: I don't know how to test this (?)
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun uri_to_route_null() {
|
||||
val actual = nip19.uriToRoute(null)
|
||||
|
||||
Assert.assertEquals(null, actual)
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun uri_to_route_unknown() {
|
||||
val actual = nip19.uriToRoute("nostr:unknown")
|
||||
|
||||
Assert.assertEquals(null, actual)
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun uri_to_route_npub() {
|
||||
val actual =
|
||||
nip19.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y")
|
||||
|
||||
Assert.assertEquals(Nip19.Type.USER, actual?.type)
|
||||
Assert.assertEquals(
|
||||
"bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365",
|
||||
actual?.hex
|
||||
)
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun uri_to_route_note() {
|
||||
val actual =
|
||||
nip19.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv")
|
||||
|
||||
Assert.assertEquals(Nip19.Type.NOTE, actual?.type)
|
||||
Assert.assertEquals(
|
||||
"82c19ee9db7644829b4420f40b72ce2d32e5b7962744b261d0d53e770432dac3",
|
||||
actual?.hex
|
||||
)
|
||||
}
|
||||
|
||||
@Ignore("Test not implemented yet")
|
||||
@Test()
|
||||
fun uri_to_route_nprofile() {
|
||||
val actual = nip19.uriToRoute("nostr:nprofile")
|
||||
|
||||
Assert.assertEquals(Nip19.Type.USER, actual?.type)
|
||||
Assert.assertEquals("*", actual?.hex)
|
||||
}
|
||||
|
||||
@Ignore("Test not implemented yet")
|
||||
@Test()
|
||||
fun uri_to_route_nevent() {
|
||||
val actual = nip19.uriToRoute("nostr:nevent")
|
||||
|
||||
Assert.assertEquals(Nip19.Type.USER, actual?.type)
|
||||
Assert.assertEquals("*", actual?.hex)
|
||||
}
|
||||
|
||||
@Ignore("Test not implemented yet")
|
||||
@Test()
|
||||
fun uri_to_route_nrelay() {
|
||||
val actual = nip19.uriToRoute("nostr:nrelay")
|
||||
|
||||
Assert.assertEquals(Nip19.Type.RELAY, actual?.type)
|
||||
Assert.assertEquals("*", actual?.hex)
|
||||
}
|
||||
|
||||
@Ignore("Test not implemented yet")
|
||||
@Test()
|
||||
fun uri_to_route_naddr() {
|
||||
val actual = nip19.uriToRoute("nostr:naddr")
|
||||
|
||||
Assert.assertEquals(Nip19.Type.ADDRESS, actual?.type)
|
||||
Assert.assertEquals("*", actual?.hex)
|
||||
}
|
||||
|
||||
private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
|
||||
}
|
||||
Reference in New Issue
Block a user