Support for User Tags on posts.

This commit is contained in:
Vitor Pamplona
2023-01-16 10:51:10 -05:00
parent f2b831a119
commit f9b86585be
14 changed files with 160 additions and 93 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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()

View File

@@ -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)
)
}

View File

@@ -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),
)
}

View File

@@ -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)
)
}

View File

@@ -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) {

View File

@@ -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 ")
} }
} }

View File

@@ -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),
)
} }
} }
} }
} }
} }

View File

@@ -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
)
}
}
}

View File

@@ -44,7 +44,6 @@ fun ZoomableAsyncImage(imageUrl: String) {
} }
} }
) { ) {
AsyncImage( AsyncImage(
model = imageUrl, model = imageUrl,
contentDescription = "Profile Image", contentDescription = "Profile Image",

View File

@@ -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, ":")
} }