Support for NIP-30
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
class NIP30Parser {
|
||||||
|
val customEmojiPattern: Pattern = Pattern.compile("\\:([A-Za-z0-9_\\-]+)\\:", Pattern.CASE_INSENSITIVE)
|
||||||
|
|
||||||
|
fun buildArray(input: String): List<String> {
|
||||||
|
val matcher = customEmojiPattern.matcher(input)
|
||||||
|
val list = mutableListOf<String>()
|
||||||
|
while (matcher.find()) {
|
||||||
|
list.add(matcher.group())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return listOf(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
val regularChars = input.split(customEmojiPattern.toRegex())
|
||||||
|
|
||||||
|
var finalList = mutableListOf<String>()
|
||||||
|
var index = 0
|
||||||
|
for (e in regularChars) {
|
||||||
|
finalList.add(e)
|
||||||
|
if (index < list.size) {
|
||||||
|
finalList.add(list[index])
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return finalList
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -380,8 +380,9 @@ fun Notifying(baseMentions: List<User>?, onClick: (User) -> Unit) {
|
|||||||
onClick(myUser)
|
onClick(myUser)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
CreateTextWithEmoji(
|
||||||
"✖ ${myUser.toBestDisplayName()}",
|
text = "✖ ${myUser.toBestDisplayName()}",
|
||||||
|
tags = myUser.info?.latestMetadata?.tags,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
|
import androidx.compose.material.LocalContentAlpha
|
||||||
|
import androidx.compose.material.LocalContentColor
|
||||||
import androidx.compose.material.LocalTextStyle
|
import androidx.compose.material.LocalTextStyle
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
@@ -11,12 +19,31 @@ import androidx.compose.runtime.livedata.observeAsState
|
|||||||
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.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.takeOrElse
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLayoutResult
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import coil.compose.AsyncImage
|
||||||
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.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.NIP30Parser
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
@@ -64,28 +91,28 @@ private fun DisplayEvent(
|
|||||||
if (note.event is ChannelCreateEvent) {
|
if (note.event is ChannelCreateEvent) {
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
note.idDisplayNote(),
|
note.idDisplayNote(),
|
||||||
nip19.additionalChars,
|
"${nip19.additionalChars} ",
|
||||||
"Channel/${nip19.hex}",
|
"Channel/${nip19.hex}",
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
} else if (note.event is PrivateDmEvent) {
|
} else if (note.event is PrivateDmEvent) {
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
note.idDisplayNote(),
|
note.idDisplayNote(),
|
||||||
nip19.additionalChars,
|
"${nip19.additionalChars} ",
|
||||||
"Room/${note.author?.pubkeyHex}",
|
"Room/${note.author?.pubkeyHex}",
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
} else if (channel != null) {
|
} else if (channel != null) {
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
channel.toBestDisplayName(),
|
channel.toBestDisplayName(),
|
||||||
nip19.additionalChars,
|
"${nip19.additionalChars} ",
|
||||||
"Channel/${note.channel()?.idHex}",
|
"Channel/${note.channel()?.idHex}",
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
note.idDisplayNote(),
|
note.idDisplayNote(),
|
||||||
nip19.additionalChars,
|
"${nip19.additionalChars} ",
|
||||||
"Event/${nip19.hex}",
|
"Event/${nip19.hex}",
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
@@ -114,34 +141,34 @@ private fun DisplayNote(
|
|||||||
|
|
||||||
noteBase?.let {
|
noteBase?.let {
|
||||||
val noteState by it.live().metadata.observeAsState()
|
val noteState by it.live().metadata.observeAsState()
|
||||||
val note = noteState?.note ?: return
|
val note = remember(noteState) { noteState?.note } ?: return
|
||||||
val channel = note.channel()
|
val channel = note.channel()
|
||||||
|
|
||||||
if (note.event is ChannelCreateEvent) {
|
if (note.event is ChannelCreateEvent) {
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
note.idDisplayNote(),
|
note.idDisplayNote(),
|
||||||
nip19.additionalChars,
|
"${nip19.additionalChars} ",
|
||||||
"Channel/${nip19.hex}",
|
"Channel/${nip19.hex}",
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
} else if (note.event is PrivateDmEvent) {
|
} else if (note.event is PrivateDmEvent) {
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
note.idDisplayNote(),
|
note.idDisplayNote(),
|
||||||
nip19.additionalChars,
|
"${nip19.additionalChars} ",
|
||||||
"Room/${note.author?.pubkeyHex}",
|
"Room/${note.author?.pubkeyHex}",
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
} else if (channel != null) {
|
} else if (channel != null) {
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
channel.toBestDisplayName(),
|
channel.toBestDisplayName(),
|
||||||
nip19.additionalChars,
|
"${nip19.additionalChars} ",
|
||||||
"Channel/${note.channel()?.idHex}",
|
"Channel/${note.channel()?.idHex}",
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
note.idDisplayNote(),
|
note.idDisplayNote(),
|
||||||
nip19.additionalChars,
|
"${nip19.additionalChars} ",
|
||||||
"Note/${nip19.hex}",
|
"Note/${nip19.hex}",
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
@@ -174,7 +201,7 @@ private fun DisplayAddress(
|
|||||||
|
|
||||||
CreateClickableText(
|
CreateClickableText(
|
||||||
note.idDisplayNote(),
|
note.idDisplayNote(),
|
||||||
nip19.additionalChars,
|
"${nip19.additionalChars} ",
|
||||||
"Note/${nip19.hex}",
|
"Note/${nip19.hex}",
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
@@ -202,14 +229,19 @@ private fun DisplayUser(
|
|||||||
|
|
||||||
userBase?.let {
|
userBase?.let {
|
||||||
val userState by it.live().metadata.observeAsState()
|
val userState by it.live().metadata.observeAsState()
|
||||||
val user = userState?.user ?: return
|
val route = remember { "User/${it.pubkeyHex}" }
|
||||||
|
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
|
||||||
|
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
||||||
|
|
||||||
CreateClickableText(
|
if (userDisplayName != null) {
|
||||||
user.toBestDisplayName(),
|
CreateClickableTextWithEmoji(
|
||||||
nip19.additionalChars,
|
clickablePart = userDisplayName,
|
||||||
"User/${nip19.hex}",
|
suffix = "${nip19.additionalChars} ",
|
||||||
navController
|
tags = userTags,
|
||||||
)
|
route = route,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userBase == null) {
|
if (userBase == null) {
|
||||||
@@ -231,9 +263,264 @@ fun CreateClickableText(clickablePart: String, suffix: String, route: String, na
|
|||||||
withStyle(
|
withStyle(
|
||||||
LocalTextStyle.current.copy(color = MaterialTheme.colors.onBackground).toSpanStyle()
|
LocalTextStyle.current.copy(color = MaterialTheme.colors.onBackground).toSpanStyle()
|
||||||
) {
|
) {
|
||||||
append("$suffix ")
|
append(suffix)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = { navController.navigate(route) }
|
onClick = { navController.navigate(route) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CreateTextWithEmoji(
|
||||||
|
text: String,
|
||||||
|
tags: List<List<String>>?,
|
||||||
|
color: Color = Color.Unspecified,
|
||||||
|
textAlign: TextAlign? = null,
|
||||||
|
fontWeight: FontWeight? = null,
|
||||||
|
fontSize: TextUnit = TextUnit.Unspecified,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
overflow: TextOverflow = TextOverflow.Clip,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val emojis = remember {
|
||||||
|
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateTextWithEmoji(text, emojis, color, textAlign, fontWeight, fontSize, maxLines, overflow, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CreateTextWithEmoji(
|
||||||
|
text: String,
|
||||||
|
emojis: Map<String, String>,
|
||||||
|
color: Color = Color.Unspecified,
|
||||||
|
textAlign: TextAlign? = null,
|
||||||
|
fontWeight: FontWeight? = null,
|
||||||
|
fontSize: TextUnit = TextUnit.Unspecified,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
overflow: TextOverflow = TextOverflow.Clip,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val textColor = color.takeOrElse {
|
||||||
|
LocalTextStyle.current.color.takeOrElse {
|
||||||
|
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emojis.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = textColor,
|
||||||
|
textAlign = textAlign,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
fontSize = fontSize,
|
||||||
|
maxLines = maxLines,
|
||||||
|
overflow = overflow,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val myList = remember {
|
||||||
|
assembleAnnotatedList(text, emojis)
|
||||||
|
}
|
||||||
|
|
||||||
|
val style = LocalTextStyle.current.merge(
|
||||||
|
TextStyle(
|
||||||
|
color = textColor,
|
||||||
|
textAlign = textAlign,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
fontSize = fontSize
|
||||||
|
)
|
||||||
|
).toSpanStyle()
|
||||||
|
|
||||||
|
InLineIconRenderer(myList, style, maxLines, overflow, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CreateClickableTextWithEmoji(
|
||||||
|
clickablePart: String,
|
||||||
|
tags: List<List<String>>?,
|
||||||
|
style: TextStyle,
|
||||||
|
onClick: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
val emojis = remember(tags) {
|
||||||
|
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emojis.isEmpty()) {
|
||||||
|
ClickableText(
|
||||||
|
AnnotatedString(clickablePart),
|
||||||
|
style = style,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val myList = remember {
|
||||||
|
assembleAnnotatedList(clickablePart, emojis)
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableInLineIconRenderer(myList, style.toSpanStyle()) {
|
||||||
|
onClick(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CreateClickableTextWithEmoji(
|
||||||
|
clickablePart: String,
|
||||||
|
suffix: String,
|
||||||
|
tags: List<List<String>>?,
|
||||||
|
fontWeight: FontWeight = FontWeight.Normal,
|
||||||
|
route: String,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
val emojis = remember(tags) {
|
||||||
|
tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emojis.isEmpty()) {
|
||||||
|
CreateClickableText(clickablePart, suffix, route, navController)
|
||||||
|
} else {
|
||||||
|
val myList = remember {
|
||||||
|
assembleAnnotatedList(clickablePart, emojis)
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableInLineIconRenderer(myList, LocalTextStyle.current.copy(color = MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()) {
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
val myList2 = remember {
|
||||||
|
assembleAnnotatedList(suffix, emojis)
|
||||||
|
}
|
||||||
|
|
||||||
|
InLineIconRenderer(myList2, LocalTextStyle.current.copy(color = MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assembleAnnotatedList(text: String, emojis: Map<String, String>): List<Renderable> {
|
||||||
|
return NIP30Parser().buildArray(text).map {
|
||||||
|
val url = emojis[it]
|
||||||
|
if (url != null) {
|
||||||
|
ImageUrlType(url)
|
||||||
|
} else {
|
||||||
|
TextType(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Renderable()
|
||||||
|
class TextType(val text: String) : Renderable()
|
||||||
|
class ImageUrlType(val url: String) : Renderable()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClickableInLineIconRenderer(wordsInOrder: List<Renderable>, style: SpanStyle, onClick: (Int) -> Unit) {
|
||||||
|
val inlineContent = wordsInOrder.mapIndexedNotNull { idx, value ->
|
||||||
|
if (value is ImageUrlType) {
|
||||||
|
Pair(
|
||||||
|
"inlineContent$idx",
|
||||||
|
InlineTextContent(
|
||||||
|
Placeholder(
|
||||||
|
width = 17.sp,
|
||||||
|
height = 17.sp,
|
||||||
|
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = value.url,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(1.dp),
|
||||||
|
colorFilter = ColorFilter.tint(style.color)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.associate { it.first to it.second }
|
||||||
|
|
||||||
|
val annotatedText = buildAnnotatedString {
|
||||||
|
wordsInOrder.forEachIndexed { idx, value ->
|
||||||
|
withStyle(
|
||||||
|
style
|
||||||
|
) {
|
||||||
|
if (value is TextType) {
|
||||||
|
append(value.text)
|
||||||
|
} else if (value is ImageUrlType) {
|
||||||
|
appendInlineContent("inlineContent$idx", "[icon]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
|
val pressIndicator = Modifier.pointerInput(onClick) {
|
||||||
|
detectTapGestures { pos ->
|
||||||
|
layoutResult.value?.let { layoutResult ->
|
||||||
|
onClick(layoutResult.getOffsetForPosition(pos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicText(
|
||||||
|
text = annotatedText,
|
||||||
|
modifier = pressIndicator,
|
||||||
|
inlineContent = inlineContent,
|
||||||
|
onTextLayout = {
|
||||||
|
layoutResult.value = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InLineIconRenderer(
|
||||||
|
wordsInOrder: List<Renderable>,
|
||||||
|
style: SpanStyle,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
overflow: TextOverflow = TextOverflow.Clip,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val inlineContent = wordsInOrder.mapIndexedNotNull { idx, value ->
|
||||||
|
if (value is ImageUrlType) {
|
||||||
|
Pair(
|
||||||
|
"inlineContent$idx",
|
||||||
|
InlineTextContent(
|
||||||
|
Placeholder(
|
||||||
|
width = 17.sp,
|
||||||
|
height = 17.sp,
|
||||||
|
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = value.url,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(1.dp),
|
||||||
|
colorFilter = ColorFilter.tint(style.color)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.associate { it.first to it.second }
|
||||||
|
|
||||||
|
val annotatedText = buildAnnotatedString {
|
||||||
|
wordsInOrder.forEachIndexed { idx, value ->
|
||||||
|
withStyle(
|
||||||
|
style
|
||||||
|
) {
|
||||||
|
if (value is TextType) {
|
||||||
|
append(value.text)
|
||||||
|
} else if (value is ImageUrlType) {
|
||||||
|
appendInlineContent("inlineContent$idx", "[icon]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = annotatedText,
|
||||||
|
inlineContent = inlineContent,
|
||||||
|
maxLines = maxLines,
|
||||||
|
overflow = overflow,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ class RichTextViewerState(
|
|||||||
val content: String,
|
val content: String,
|
||||||
val urlSet: Set<String>,
|
val urlSet: Set<String>,
|
||||||
val imagesForPager: Map<String, ZoomableUrlContent>,
|
val imagesForPager: Map<String, ZoomableUrlContent>,
|
||||||
val imageList: List<ZoomableUrlContent>
|
val imageList: List<ZoomableUrlContent>,
|
||||||
|
val customEmoji: Map<String, String>
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -122,7 +123,7 @@ private fun RenderRegular(
|
|||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
var processedState by remember {
|
var processedState by remember {
|
||||||
mutableStateOf<RichTextViewerState?>(RichTextViewerState(content, emptySet(), emptyMap(), emptyList()))
|
mutableStateOf<RichTextViewerState?>(RichTextViewerState(content, emptySet(), emptyMap(), emptyList(), emptyMap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -143,8 +144,10 @@ private fun RenderRegular(
|
|||||||
}.associateBy { it.url }
|
}.associateBy { it.url }
|
||||||
val imageList = imagesForPager.values.toList()
|
val imageList = imagesForPager.values.toList()
|
||||||
|
|
||||||
if (urlSet.isNotEmpty()) {
|
val emojiMap = tags?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] } ?: emptyMap()
|
||||||
processedState = RichTextViewerState(content, urlSet, imagesForPager, imageList)
|
|
||||||
|
if (urlSet.isNotEmpty() || emojiMap.isNotEmpty()) {
|
||||||
|
processedState = RichTextViewerState(content, urlSet, imagesForPager, imageList, emojiMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +170,8 @@ private fun RenderRegular(
|
|||||||
ZoomableContentView(img, state.imageList)
|
ZoomableContentView(img, state.imageList)
|
||||||
} else if (state.urlSet.contains(word)) {
|
} else if (state.urlSet.contains(word)) {
|
||||||
UrlPreview(word, "$word ")
|
UrlPreview(word, "$word ")
|
||||||
|
} else if (state.customEmoji.any { word.contains(it.key) }) {
|
||||||
|
RenderCustomEmoji(word, state.customEmoji)
|
||||||
} else if (word.startsWith("lnbc", true)) {
|
} else if (word.startsWith("lnbc", true)) {
|
||||||
MayBeInvoicePreview(word)
|
MayBeInvoicePreview(word)
|
||||||
} else if (word.startsWith("lnurl", true)) {
|
} else if (word.startsWith("lnurl", true)) {
|
||||||
@@ -235,6 +240,8 @@ private fun RenderRegular(
|
|||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (state.customEmoji.any { word.contains(it.key) }) {
|
||||||
|
RenderCustomEmoji(word, state.customEmoji)
|
||||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||||
ClickableEmail(word)
|
ClickableEmail(word)
|
||||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||||
@@ -293,6 +300,14 @@ private fun RenderRegular(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderCustomEmoji(word: String, customEmoji: Map<String, String>) {
|
||||||
|
CreateTextWithEmoji(
|
||||||
|
text = "$word ",
|
||||||
|
emojis = customEmoji
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) {
|
private fun RenderContentAsMarkdown(content: String, backgroundColor: Color) {
|
||||||
val myMarkDownStyle = richTextDefaults.copy(
|
val myMarkDownStyle = richTextDefaults.copy(
|
||||||
@@ -600,8 +615,10 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
|
|||||||
var baseUserPair by remember { mutableStateOf<Pair<User, String?>?>(null) }
|
var baseUserPair by remember { mutableStateOf<Pair<User, String?>?>(null) }
|
||||||
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) }
|
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(key1 = word) {
|
LaunchedEffect(key1 = word) {
|
||||||
withContext(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
val matcher = tagIndex.matcher(word)
|
val matcher = tagIndex.matcher(word)
|
||||||
val (index, suffix) = try {
|
val (index, suffix) = try {
|
||||||
matcher.find()
|
matcher.find()
|
||||||
@@ -630,8 +647,24 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
|
|||||||
}
|
}
|
||||||
|
|
||||||
baseUserPair?.let {
|
baseUserPair?.let {
|
||||||
ClickableUserTag(it.first, navController)
|
val innerUserState by it.first.live().metadata.observeAsState()
|
||||||
Text(text = "${it.second} ")
|
val displayName = remember(innerUserState) {
|
||||||
|
innerUserState?.user?.toBestDisplayName() ?: ""
|
||||||
|
}
|
||||||
|
val route = remember(innerUserState) {
|
||||||
|
"User/${it.first.pubkeyHex}"
|
||||||
|
}
|
||||||
|
val userTags = remember(innerUserState) {
|
||||||
|
innerUserState?.user?.info?.latestMetadata?.tags
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateClickableTextWithEmoji(
|
||||||
|
clickablePart = displayName,
|
||||||
|
suffix = "${it.second} ",
|
||||||
|
tags = userTags,
|
||||||
|
route = route,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseNotePair?.let {
|
baseNotePair?.let {
|
||||||
|
|||||||
+5
-1
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.decodePublicKey
|
import com.vitorpamplona.amethyst.model.decodePublicKey
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
@@ -149,7 +150,10 @@ fun AccountSwitchBottomSheet(
|
|||||||
val npubShortHex = acc.npub.toShortenHex()
|
val npubShortHex = acc.npub.toShortenHex()
|
||||||
|
|
||||||
user.bestDisplayName()?.let {
|
user.bestDisplayName()?.let {
|
||||||
Text(it)
|
CreateTextWithEmoji(
|
||||||
|
text = it,
|
||||||
|
tags = user.info?.latestMetadata?.tags
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(npubShortHex)
|
Text(npubShortHex)
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.ServiceManager
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClient
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
|
||||||
@@ -168,8 +169,9 @@ fun ProfileContent(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
if (accountUser.bestDisplayName() != null) {
|
if (accountUser.bestDisplayName() != null) {
|
||||||
Text(
|
CreateTextWithEmoji(
|
||||||
accountUser.bestDisplayName() ?: "",
|
text = accountUser.bestDisplayName() ?: "",
|
||||||
|
tags = accountUser.info?.latestMetadata?.tags,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 7.dp)
|
.padding(top = 7.dp)
|
||||||
.clickable(onClick = {
|
.clickable(onClick = {
|
||||||
@@ -185,19 +187,22 @@ fun ProfileContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (accountUser.bestUsername() != null) {
|
if (accountUser.bestUsername() != null) {
|
||||||
Text(
|
CreateTextWithEmoji(
|
||||||
" @${accountUser.bestUsername()}",
|
text = " @${accountUser.bestUsername()}",
|
||||||
|
tags = accountUser.info?.latestMetadata?.tags,
|
||||||
color = Color.LightGray,
|
color = Color.LightGray,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 15.dp)
|
.padding(top = 15.dp)
|
||||||
.clickable(onClick = {
|
.clickable(
|
||||||
accountUser.let {
|
onClick = {
|
||||||
navController.navigate("User/${it.pubkeyHex}")
|
accountUser.let {
|
||||||
|
navController.navigate("User/${it.pubkeyHex}")
|
||||||
|
}
|
||||||
|
coroutineScope.launch {
|
||||||
|
scaffoldState.drawerState.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
coroutineScope.launch {
|
)
|
||||||
scaffoldState.drawerState.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
@@ -218,14 +220,13 @@ fun ChatroomMessageCompose(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
CreateClickableTextWithEmoji(
|
||||||
" ${author.toBestDisplayName()}",
|
clickablePart = " ${author.toBestDisplayName()}",
|
||||||
|
suffix = "",
|
||||||
|
tags = author.info?.latestMetadata?.tags,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.clickable(onClick = {
|
route = "User/${author.pubkeyHex}",
|
||||||
author.let {
|
navController = navController
|
||||||
navController.navigate("User/${it.pubkeyHex}")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,33 +252,39 @@ fun ChatroomMessageCompose(
|
|||||||
val event = note.event
|
val event = note.event
|
||||||
if (event is ChannelCreateEvent) {
|
if (event is ChannelCreateEvent) {
|
||||||
val channelInfo = event.channelInfo()
|
val channelInfo = event.channelInfo()
|
||||||
Text(
|
val text = note.author?.toBestDisplayName()
|
||||||
text = note.author?.toBestDisplayName()
|
.toString() + " ${stringResource(R.string.created)} " + (
|
||||||
.toString() + " ${stringResource(R.string.created)} " + (
|
channelInfo.name
|
||||||
channelInfo.name
|
?: ""
|
||||||
?: ""
|
) + " ${stringResource(R.string.with_description_of)} '" + (
|
||||||
) + " ${stringResource(R.string.with_description_of)} '" + (
|
channelInfo.about
|
||||||
channelInfo.about
|
?: ""
|
||||||
?: ""
|
) + "', ${stringResource(R.string.and_picture)} '" + (
|
||||||
) + "', ${stringResource(R.string.and_picture)} '" + (
|
channelInfo.picture
|
||||||
channelInfo.picture
|
?: ""
|
||||||
?: ""
|
) + "'"
|
||||||
) + "'"
|
|
||||||
|
CreateTextWithEmoji(
|
||||||
|
text = text,
|
||||||
|
tags = note.author?.info?.latestMetadata?.tags
|
||||||
)
|
)
|
||||||
} else if (event is ChannelMetadataEvent) {
|
} else if (event is ChannelMetadataEvent) {
|
||||||
val channelInfo = event.channelInfo()
|
val channelInfo = event.channelInfo()
|
||||||
Text(
|
val text = note.author?.toBestDisplayName()
|
||||||
text = note.author?.toBestDisplayName()
|
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (
|
||||||
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (
|
channelInfo.name
|
||||||
channelInfo.name
|
?: ""
|
||||||
?: ""
|
) + "', ${stringResource(R.string.description_to)} '" + (
|
||||||
) + "$', {stringResource(R.string.description_to)} '" + (
|
channelInfo.about
|
||||||
channelInfo.about
|
?: ""
|
||||||
?: ""
|
) + "', ${stringResource(R.string.and_picture_to)} '" + (
|
||||||
) + "', ${stringResource(R.string.and_picture_to)} '" + (
|
channelInfo.picture
|
||||||
channelInfo.picture
|
?: ""
|
||||||
?: ""
|
) + "'"
|
||||||
) + "'"
|
|
||||||
|
CreateTextWithEmoji(
|
||||||
|
text = text,
|
||||||
|
tags = note.author?.info?.latestMetadata?.tags
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val eventContent = accountViewModel.decrypt(note)
|
val eventContent = accountViewModel.decrypt(note)
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ import com.vitorpamplona.amethyst.service.model.ReportedKey
|
|||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableText
|
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||||
@@ -1245,13 +1245,15 @@ fun DisplayHighlight(
|
|||||||
val userState by userBase.live().metadata.observeAsState()
|
val userState by userBase.live().metadata.observeAsState()
|
||||||
val route = remember { "User/${userBase.pubkeyHex}" }
|
val route = remember { "User/${userBase.pubkeyHex}" }
|
||||||
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
|
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
|
||||||
|
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
||||||
|
|
||||||
if (userDisplayName != null) {
|
if (userDisplayName != null) {
|
||||||
CreateClickableText(
|
CreateClickableTextWithEmoji(
|
||||||
userDisplayName,
|
clickablePart = userDisplayName,
|
||||||
"",
|
suffix = "",
|
||||||
route,
|
tags = userTags,
|
||||||
navController
|
route = route,
|
||||||
|
navController = navController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.navigation.NavController
|
|||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.*
|
import com.vitorpamplona.amethyst.model.*
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -59,8 +60,9 @@ fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Ac
|
|||||||
val innerUser = innerUserState?.user
|
val innerUser = innerUserState?.user
|
||||||
|
|
||||||
innerUser?.let { myUser ->
|
innerUser?.let { myUser ->
|
||||||
ClickableText(
|
CreateClickableTextWithEmoji(
|
||||||
AnnotatedString("$prefix@${myUser.toBestDisplayName()}"),
|
clickablePart = "$prefix@${myUser.toBestDisplayName()}",
|
||||||
|
tags = myUser.info?.latestMetadata?.tags,
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f), fontSize = 13.sp),
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f), fontSize = 13.sp),
|
||||||
onClick = { onUserTagClick(myUser) }
|
onClick = { onUserTagClick(myUser) }
|
||||||
)
|
)
|
||||||
@@ -196,8 +198,9 @@ fun ReplyInformationChannel(
|
|||||||
val innerUser = innerUserState?.user
|
val innerUser = innerUserState?.user
|
||||||
|
|
||||||
innerUser?.let { myUser ->
|
innerUser?.let { myUser ->
|
||||||
ClickableText(
|
CreateClickableTextWithEmoji(
|
||||||
AnnotatedString("$prefix@${myUser.toBestDisplayName()}"),
|
clickablePart = "$prefix@${myUser.toBestDisplayName()}",
|
||||||
|
tags = myUser.info?.latestMetadata?.tags,
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f), fontSize = 13.sp),
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f), fontSize = 13.sp),
|
||||||
onClick = { onUserTagClick(myUser) }
|
onClick = { onUserTagClick(myUser) }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,34 +5,31 @@ import androidx.compose.material.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
|
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
|
||||||
val noteState by baseNote.live().metadata.observeAsState()
|
val noteState by baseNote.live().metadata.observeAsState()
|
||||||
val note = noteState?.note ?: return
|
val author = remember(noteState) { noteState?.note?.author } ?: return
|
||||||
|
|
||||||
val author = note.author
|
UsernameDisplay(author, weight)
|
||||||
|
|
||||||
if (author != null) {
|
|
||||||
UsernameDisplay(author, weight)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
|
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
|
||||||
val userState by baseUser.live().metadata.observeAsState()
|
val userState by baseUser.live().metadata.observeAsState()
|
||||||
val user = userState?.user ?: return
|
val bestUserName = remember(userState) { userState?.user?.bestUsername() }
|
||||||
|
val bestDisplayName = remember(userState) { userState?.user?.bestDisplayName() }
|
||||||
|
val npubDisplay = remember { baseUser.pubkeyDisplayHex() }
|
||||||
|
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
||||||
|
|
||||||
val bestUserName = user.bestUsername()
|
UserNameDisplay(bestUserName, bestDisplayName, npubDisplay, tags, weight)
|
||||||
val bestDisplayName = user.bestDisplayName()
|
|
||||||
val npubDisplay = user.pubkeyDisplayHex()
|
|
||||||
|
|
||||||
UserNameDisplay(bestUserName, bestDisplayName, npubDisplay, weight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -40,31 +37,36 @@ private fun UserNameDisplay(
|
|||||||
bestUserName: String?,
|
bestUserName: String?,
|
||||||
bestDisplayName: String?,
|
bestDisplayName: String?,
|
||||||
npubDisplay: String,
|
npubDisplay: String,
|
||||||
|
tags: List<List<String>>?,
|
||||||
modifier: Modifier
|
modifier: Modifier
|
||||||
) {
|
) {
|
||||||
if (bestUserName != null && bestDisplayName != null) {
|
if (bestUserName != null && bestDisplayName != null) {
|
||||||
Text(
|
CreateTextWithEmoji(
|
||||||
bestDisplayName,
|
text = bestDisplayName,
|
||||||
|
tags = tags,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Text(
|
CreateTextWithEmoji(
|
||||||
"@$bestUserName",
|
text = "@$bestUserName",
|
||||||
|
tags = tags,
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
} else if (bestDisplayName != null) {
|
} else if (bestDisplayName != null) {
|
||||||
Text(
|
CreateTextWithEmoji(
|
||||||
bestDisplayName,
|
text = bestDisplayName,
|
||||||
|
tags = tags,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
} else if (bestUserName != null) {
|
} else if (bestUserName != null) {
|
||||||
Text(
|
CreateTextWithEmoji(
|
||||||
"@$bestUserName",
|
text = "@$bestUserName",
|
||||||
|
tags = tags,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||||
import com.vitorpamplona.amethyst.ui.qrcode.NIP19QrCodeScanner
|
import com.vitorpamplona.amethyst.ui.qrcode.NIP19QrCodeScanner
|
||||||
@@ -83,10 +84,10 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
|||||||
.background(MaterialTheme.colors.background)
|
.background(MaterialTheme.colors.background)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) {
|
||||||
Text(
|
CreateTextWithEmoji(
|
||||||
user.bestDisplayName() ?: "",
|
text = user.bestDisplayName() ?: user.bestUsername() ?: "",
|
||||||
modifier = Modifier.padding(top = 5.dp),
|
tags = user.info?.latestMetadata?.tags,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 18.sp
|
fontSize = 18.sp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.service.model.PayInvoiceErrorResponse
|
|||||||
import com.vitorpamplona.amethyst.service.model.PayInvoiceSuccessResponse
|
import com.vitorpamplona.amethyst.service.model.PayInvoiceSuccessResponse
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
|
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
|
||||||
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
|
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
|
||||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||||
@@ -470,29 +471,36 @@ private fun ProfileActions(
|
|||||||
private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewModel: AccountViewModel, navController: NavController) {
|
private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewModel: AccountViewModel, navController: NavController) {
|
||||||
val userState by baseUser.live().metadata.observeAsState()
|
val userState by baseUser.live().metadata.observeAsState()
|
||||||
val user = remember(userState) { userState?.user } ?: return
|
val user = remember(userState) { userState?.user } ?: return
|
||||||
|
val tags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
|
||||||
|
|
||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.Bottom) {
|
(user.bestDisplayName() ?: user.bestUsername())?.let {
|
||||||
user.bestDisplayName()?.let {
|
Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 7.dp)) {
|
||||||
Text(
|
CreateTextWithEmoji(
|
||||||
it,
|
text = it,
|
||||||
modifier = Modifier.padding(top = 7.dp),
|
tags = tags,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 25.sp
|
fontSize = 25.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
|
if (user.bestDisplayName() != null) {
|
||||||
user.bestUsername()?.let {
|
user.bestUsername()?.let {
|
||||||
Text(
|
Row(
|
||||||
"@$it",
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
|
||||||
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp)
|
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp)
|
||||||
)
|
) {
|
||||||
|
CreateTextWithEmoji(
|
||||||
|
text = "@$it",
|
||||||
|
tags = tags,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class Nip30Test {
|
||||||
|
@Test()
|
||||||
|
fun parseEmoji() {
|
||||||
|
val input = "Alex Gleason :soapbox:"
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf("Alex Gleason ", ":soapbox:", ""),
|
||||||
|
NIP30Parser().buildArray(input)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun parseEmojiInverted() {
|
||||||
|
val input = ":soapbox:Alex Gleason"
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf("", ":soapbox:", "Alex Gleason"),
|
||||||
|
NIP30Parser().buildArray(input)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun parseEmoji2() {
|
||||||
|
val input = "Hello :gleasonator: \uD83D\uDE02 :ablobcatrainbow: :disputed: yolo"
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf("Hello ", ":gleasonator:", " 😂 ", ":ablobcatrainbow:", " ", ":disputed:", " yolo"),
|
||||||
|
NIP30Parser().buildArray(input)
|
||||||
|
)
|
||||||
|
|
||||||
|
println(NIP30Parser().buildArray(input).joinToString(","))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun parseEmoji3() {
|
||||||
|
val input = "hello vitor: how can I help:"
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf("hello vitor: how can I help:"),
|
||||||
|
NIP30Parser().buildArray(input)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun parseJapanese() {
|
||||||
|
val input = "\uD883\uDEDE\uD883\uDEDE麺の:x30EDE:。:\uD883\uDEDE:(Violation of NIP-30)"
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
listOf("\uD883\uDEDE\uD883\uDEDE麺の", ":x30EDE:", "。:\uD883\uDEDE:(Violation of NIP-30)"),
|
||||||
|
NIP30Parser().buildArray(input)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user