mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-20 14:01:22 +02:00
Support for User Tags on posts.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.lnurl
|
||||||
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Locale
|
import java.util.Locale
|
@@ -1,6 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.toDisplayHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import nostr.postr.Bech32
|
import nostr.postr.Bech32
|
||||||
@@ -22,7 +22,7 @@ fun HexKey.toByteArray(): ByteArray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun HexKey.toDisplayHexKey(): String {
|
fun HexKey.toDisplayHexKey(): String {
|
||||||
return this.toDisplayHex()
|
return this.toShortenHex()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decodePublicKey(key: String): ByteArray {
|
fun decodePublicKey(key: String): ByteArray {
|
||||||
|
@@ -2,7 +2,7 @@ package com.vitorpamplona.amethyst.model
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.note.toDisplayHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -18,7 +18,7 @@ class Note(val idHex: String) {
|
|||||||
// These fields are always available.
|
// These fields are always available.
|
||||||
// They are immutable
|
// They are immutable
|
||||||
val id = Hex.decode(idHex)
|
val id = Hex.decode(idHex)
|
||||||
val idDisplayHex = id.toDisplayHex()
|
val idDisplayHex = id.toShortenHex()
|
||||||
|
|
||||||
// These fields are only available after the Text Note event is received.
|
// These fields are only available after the Text Note event is received.
|
||||||
// They are immutable after that.
|
// They are immutable after that.
|
||||||
|
@@ -2,14 +2,14 @@ package com.vitorpamplona.amethyst.model
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.note.toDisplayHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import nostr.postr.events.ContactListEvent
|
import nostr.postr.events.ContactListEvent
|
||||||
|
|
||||||
class User(val pubkey: ByteArray) {
|
class User(val pubkey: ByteArray) {
|
||||||
val pubkeyHex = pubkey.toHexKey()
|
val pubkeyHex = pubkey.toHexKey()
|
||||||
val pubkeyDisplayHex = pubkey.toDisplayHex()
|
val pubkeyDisplayHex = pubkey.toShortenHex()
|
||||||
|
|
||||||
var info = UserMetadata()
|
var info = UserMetadata()
|
||||||
|
|
||||||
|
@@ -0,0 +1,26 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.material.LocalTextStyle
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.toNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClickableNoteTag(
|
||||||
|
note: Note,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
val innerNoteState by note.live.observeAsState()
|
||||||
|
ClickableText(
|
||||||
|
text = AnnotatedString("@${innerNoteState?.note?.id?.toNote()?.toShortenHex()} "),
|
||||||
|
onClick = { navController.navigate("Note/${innerNoteState?.note?.idHex}") },
|
||||||
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.material.LocalTextStyle
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClickableUrl(urlText: String, url: String) {
|
||||||
|
val uri = LocalUriHandler.current
|
||||||
|
|
||||||
|
ClickableText(
|
||||||
|
text = AnnotatedString("$urlText "),
|
||||||
|
onClick = { runCatching { uri.openUri(url) } },
|
||||||
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,24 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.material.LocalTextStyle
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClickableUserTag(
|
||||||
|
user: User,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
val innerUserState by user.live.observeAsState()
|
||||||
|
ClickableText(
|
||||||
|
text = AnnotatedString("@${innerUserState?.user?.toBestDisplayName()} "),
|
||||||
|
onClick = { navController.navigate("User/${innerUserState?.user?.pubkeyHex}") },
|
||||||
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||||
|
)
|
||||||
|
}
|
@@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InvoicePreview(lnInvoice: String) {
|
fun InvoicePreview(lnInvoice: String) {
|
||||||
|
@@ -3,22 +3,16 @@ package com.vitorpamplona.amethyst.ui.components
|
|||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.material.LocalTextStyle
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
|
import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.toNote
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import com.vitorpamplona.amethyst.ui.note.toDisplayHex
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
@@ -100,22 +94,19 @@ fun TagLink(word: String, tags: List<List<String>>, navController: NavController
|
|||||||
if (tags[index][0] == "p") {
|
if (tags[index][0] == "p") {
|
||||||
val user = LocalCache.users[tags[index][1]]
|
val user = LocalCache.users[tags[index][1]]
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
val innerUserState by user.live.observeAsState()
|
ClickableUserTag(user, navController)
|
||||||
Text(
|
} else {
|
||||||
"@${innerUserState?.user?.toBestDisplayName()} "
|
Text(text = "${tags[index][1].toShortenHex()} ")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else if (tags[index][0] == "e") {
|
} else if (tags[index][0] == "e") {
|
||||||
val note = LocalCache.notes[tags[index][1]]
|
val note = LocalCache.notes[tags[index][1]]
|
||||||
if (note != null) {
|
if (note != null) {
|
||||||
val innerNoteState by note.live.observeAsState()
|
ClickableNoteTag(note, navController)
|
||||||
ClickableText(
|
} else {
|
||||||
text = AnnotatedString("@${innerNoteState?.note?.id?.toNote()?.toDisplayHex()} "),
|
Text(text = "${tags[index][1].toShortenHex()} ")
|
||||||
onClick = { navController.navigate("Note/${note.idHex}") },
|
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
Text(text = "$word ")
|
Text(text = "$word ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,45 +1,21 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.material.LocalTextStyle
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import com.baha.url.preview.IUrlPreviewCallback
|
import com.baha.url.preview.IUrlPreviewCallback
|
||||||
import com.baha.url.preview.UrlInfoItem
|
import com.baha.url.preview.UrlInfoItem
|
||||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UrlPreview(url: String, urlText: String, showUrlIfError: Boolean = true) {
|
fun UrlPreview(url: String, urlText: String, showUrlIfError: Boolean = true) {
|
||||||
var urlPreviewState by remember { mutableStateOf<UrlPreviewState>(UrlPreviewState.Loading) }
|
var urlPreviewState by remember { mutableStateOf<UrlPreviewState>(UrlPreviewState.Loading) }
|
||||||
|
|
||||||
val uri = LocalUriHandler.current
|
|
||||||
|
|
||||||
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
|
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
|
||||||
LaunchedEffect(url) {
|
LaunchedEffect(url) {
|
||||||
UrlCachedPreviewer.previewInfo(url, object : IUrlPreviewCallback {
|
UrlCachedPreviewer.previewInfo(url, object : IUrlPreviewCallback {
|
||||||
@@ -59,50 +35,15 @@ fun UrlPreview(url: String, urlText: String, showUrlIfError: Boolean = true) {
|
|||||||
Crossfade(targetState = urlPreviewState) { state ->
|
Crossfade(targetState = urlPreviewState) { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
is UrlPreviewState.Loaded -> {
|
is UrlPreviewState.Loaded -> {
|
||||||
Row(
|
UrlPreviewCard(url, state.previewInfo)
|
||||||
modifier = Modifier.clickable { runCatching { uri.openUri(url) } }
|
|
||||||
.clip(shape = RoundedCornerShape(15.dp))
|
|
||||||
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp))
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
AsyncImage(
|
|
||||||
model = state.previewInfo.image,
|
|
||||||
contentDescription = "Profile Image",
|
|
||||||
contentScale = ContentScale.FillWidth,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = state.previewInfo.title,
|
|
||||||
style = MaterialTheme.typography.body2,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, top= 10.dp),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = state.previewInfo.description,
|
|
||||||
style = MaterialTheme.typography.caption,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
|
||||||
color = Color.Gray,
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (showUrlIfError) {
|
if (showUrlIfError) {
|
||||||
ClickableText(
|
ClickableUrl(urlText, url)
|
||||||
text = AnnotatedString("$urlText "),
|
|
||||||
onClick = { runCatching { uri.openUri(url) } },
|
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,66 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.baha.url.preview.UrlInfoItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UrlPreviewCard(
|
||||||
|
url: String,
|
||||||
|
previewInfo: UrlInfoItem
|
||||||
|
) {
|
||||||
|
val uri = LocalUriHandler.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { runCatching { uri.openUri(url) } }
|
||||||
|
.clip(shape = RoundedCornerShape(15.dp))
|
||||||
|
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp))
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
AsyncImage(
|
||||||
|
model = previewInfo.image,
|
||||||
|
contentDescription = "Profile Image",
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = previewInfo.title,
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = previewInfo.description,
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -44,7 +44,6 @@ fun ZoomableAsyncImage(imageUrl: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = imageUrl,
|
model = imageUrl,
|
||||||
contentDescription = "Profile Image",
|
contentDescription = "Profile Image",
|
||||||
|
@@ -2,11 +2,11 @@ package com.vitorpamplona.amethyst.ui.note
|
|||||||
|
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
|
|
||||||
fun ByteArray.toDisplayHex(): String {
|
fun ByteArray.toShortenHex(): String {
|
||||||
return toHex().toDisplayHex()
|
return toHex().toShortenHex()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.toDisplayHex(): String {
|
fun String.toShortenHex(): String {
|
||||||
return replaceRange(6, length-6, ":")
|
return replaceRange(6, length-6, ":")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user