mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-17 13:21:50 +01:00
Adding secret emojis to reactions
This commit is contained in:
parent
8722f37fa5
commit
9cd73c2663
@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
|
||||
|
||||
fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2
|
||||
@ -125,5 +126,9 @@ fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists<String>): String {
|
||||
}
|
||||
}
|
||||
|
||||
if (EmojiCoder.isCoded(this)) {
|
||||
return EmojiCoder.cropToFirstMessage(this)
|
||||
}
|
||||
|
||||
return firstFullChar()
|
||||
}
|
||||
|
@ -38,14 +38,19 @@ import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun AnimatedBorderTextCornerRadius(
|
||||
text: String,
|
||||
modifier: Modifier,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
textAlign: TextAlign? = null,
|
||||
fontSize: TextUnit = 12.sp,
|
||||
) {
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val animatedFloatRestart =
|
||||
@ -61,7 +66,7 @@ fun AnimatedBorderTextCornerRadius(
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 12.sp,
|
||||
fontSize = fontSize,
|
||||
modifier =
|
||||
modifier
|
||||
.drawBehind {
|
||||
@ -84,6 +89,8 @@ fun AnimatedBorderTextCornerRadius(
|
||||
.CornerRadius(6.dp.toPx()),
|
||||
)
|
||||
}.padding(3.dp),
|
||||
color = color,
|
||||
textAlign = textAlign,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
@ -53,15 +54,24 @@ 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.LocalDensity
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
||||
import com.vitorpamplona.amethyst.ui.components.CoreSecretMessage
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
@ -92,6 +102,7 @@ import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
@ -212,7 +223,18 @@ fun RenderLikeGallery(
|
||||
when (val shortReaction = reactionType) {
|
||||
"+" -> LikedIcon(modifier.size(Size19dp))
|
||||
"-" -> Text(text = "\uD83D\uDC4E", modifier = modifier)
|
||||
else -> Text(text = shortReaction, modifier = modifier)
|
||||
else -> {
|
||||
if (EmojiCoder.isCoded(shortReaction)) {
|
||||
DisplaySecretEmojiAsReaction(
|
||||
shortReaction,
|
||||
modifier,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
} else {
|
||||
Text(text = shortReaction, modifier = modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -222,6 +244,57 @@ fun RenderLikeGallery(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplaySecretEmojiAsReaction(
|
||||
reaction: String,
|
||||
modifier: Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
var secretContent by remember {
|
||||
mutableStateOf<RichTextViewerState?>(null)
|
||||
}
|
||||
|
||||
var showPopup by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
LaunchedEffect(reaction) {
|
||||
launch(Dispatchers.Default) {
|
||||
secretContent =
|
||||
CachedRichTextParser.parseText(
|
||||
EmojiCoder.decode(reaction),
|
||||
EmptyTagList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val localSecretContent = secretContent
|
||||
|
||||
AnimatedBorderTextCornerRadius(
|
||||
reaction,
|
||||
modifier.clickable {
|
||||
showPopup = !showPopup
|
||||
},
|
||||
)
|
||||
|
||||
if (localSecretContent != null && showPopup) {
|
||||
val iconSizePx = with(LocalDensity.current) { -24.dp.toPx().toInt() }
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.TopCenter,
|
||||
offset = IntOffset(0, -iconSizePx),
|
||||
onDismissRequest = { showPopup = false },
|
||||
properties = PopupProperties(focusable = true),
|
||||
) {
|
||||
Surface(Modifier.padding(10.dp)) {
|
||||
val color = remember { mutableStateOf(Color.Transparent) }
|
||||
CoreSecretMessage(localSecretContent, null, 3, color, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DecryptAndRenderZapGallery(
|
||||
multiSetCard: MultiSetCard,
|
||||
|
@ -101,11 +101,13 @@ import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
@ -953,7 +955,16 @@ private fun RenderReactionType(
|
||||
when (reactionType) {
|
||||
"+" -> LikedIcon(iconSizeModifier)
|
||||
"-" -> Text(text = "\uD83D\uDC4E", maxLines = 1, fontSize = iconFontSize)
|
||||
else -> Text(text = reactionType, maxLines = 1, fontSize = iconFontSize)
|
||||
else -> {
|
||||
if (EmojiCoder.isCoded(reactionType)) {
|
||||
AnimatedBorderTextCornerRadius(
|
||||
reactionType,
|
||||
fontSize = iconFontSize,
|
||||
)
|
||||
} else {
|
||||
Text(text = reactionType, maxLines = 1, fontSize = iconFontSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1501,6 +1512,7 @@ fun ReactionChoicePopupPeeview() {
|
||||
"\uD83D\uDE31",
|
||||
"\uD83E\uDD14",
|
||||
"\uD83D\uDE31",
|
||||
"\uD83D\uDE80\uDB40\uDD58\uDB40\uDD64\uDB40\uDD64\uDB40\uDD60\uDB40\uDD63\uDB40\uDD2A\uDB40\uDD1F\uDB40\uDD1F\uDB40\uDD53\uDB40\uDD54\uDB40\uDD5E\uDB40\uDD1E\uDB40\uDD63\uDB40\uDD51\uDB40\uDD64\uDB40\uDD55\uDB40\uDD5C\uDB40\uDD5C\uDB40\uDD59\uDB40\uDD64\uDB40\uDD55\uDB40\uDD1E\uDB40\uDD55\uDB40\uDD51\uDB40\uDD62\uDB40\uDD64\uDB40\uDD58\uDB40\uDD1F\uDB40\uDD29\uDB40\uDD24\uDB40\uDD27\uDB40\uDD55\uDB40\uDD24\uDB40\uDD51\uDB40\uDD52\uDB40\uDD22\uDB40\uDD54\uDB40\uDD23\uDB40\uDD21\uDB40\uDD21\uDB40\uDD25\uDB40\uDD52\uDB40\uDD55\uDB40\uDD25\uDB40\uDD26\uDB40\uDD25\uDB40\uDD51\uDB40\uDD24\uDB40\uDD29\uDB40\uDD53\uDB40\uDD56\uDB40\uDD25\uDB40\uDD54\uDB40\uDD52\uDB40\uDD20\uDB40\uDD22\uDB40\uDD25\uDB40\uDD25\uDB40\uDD29\uDB40\uDD56\uDB40\uDD23\uDB40\uDD21\uDB40\uDD20\uDB40\uDD53\uDB40\uDD51\uDB40\uDD20\uDB40\uDD51\uDB40\uDD26\uDB40\uDD54\uDB40\uDD54\uDB40\uDD56\uDB40\uDD54\uDB40\uDD54\uDB40\uDD52\uDB40\uDD54\uDB40\uDD24\uDB40\uDD52\uDB40\uDD54\uDB40\uDD28\uDB40\uDD53\uDB40\uDD52\uDB40\uDD55\uDB40\uDD53\uDB40\uDD24\uDB40\uDD24\uDB40\uDD29\uDB40\uDD29\uDB40\uDD25\uDB40\uDD53\uDB40\uDD22\uDB40\uDD55\uDB40\uDD27\uDB40\uDD1E\uDB40\uDD67\uDB40\uDD55\uDB40\uDD52\uDB40\uDD60",
|
||||
),
|
||||
onClick = {},
|
||||
onChangeAmount = {},
|
||||
@ -1554,12 +1566,20 @@ fun RenderReaction(reactionType: String) {
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
Text(
|
||||
reactionType,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
fontSize = 22.sp,
|
||||
)
|
||||
if (EmojiCoder.isCoded(reactionType)) {
|
||||
AnimatedBorderTextCornerRadius(
|
||||
reactionType,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
reactionType,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
fontSize = 22.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@ -77,9 +78,14 @@ import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
||||
import com.vitorpamplona.amethyst.ui.components.CoreSecretMessage
|
||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
@ -115,7 +121,13 @@ class UpdateReactionTypeViewModel : ViewModel() {
|
||||
fun toListOfChoices(commaSeparatedAmounts: String): List<Long> = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 }
|
||||
|
||||
fun addChoice() {
|
||||
val newValue = nextChoice.text.trim().firstFullChar()
|
||||
val newValue =
|
||||
if (EmojiCoder.isCoded(nextChoice.text)) {
|
||||
EmojiCoder.cropToFirstMessage(nextChoice.text)
|
||||
} else {
|
||||
nextChoice.text.trim().firstFullChar()
|
||||
}
|
||||
|
||||
reactionSet = reactionSet + newValue
|
||||
|
||||
nextChoice = TextFieldValue("")
|
||||
@ -341,14 +353,77 @@ private fun RenderReactionOption(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
else ->
|
||||
Text(
|
||||
text = "$reactionType ✖",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
else -> {
|
||||
if (EmojiCoder.isCoded(reactionType)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
AnimatedBorderTextCornerRadius(
|
||||
reactionType,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
text = " ✖",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "$reactionType ✖",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplaySecretEmoji(
|
||||
text: String,
|
||||
state: RichTextViewerState,
|
||||
callbackUri: String?,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
if (canPreview && quotesLeft > 0) {
|
||||
var secretContent by remember {
|
||||
mutableStateOf<RichTextViewerState?>(null)
|
||||
}
|
||||
|
||||
var showPopup by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
LaunchedEffect(text) {
|
||||
launch(Dispatchers.Default) {
|
||||
secretContent =
|
||||
CachedRichTextParser.parseText(
|
||||
EmojiCoder.decode(text),
|
||||
state.tags,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val localSecretContent = secretContent
|
||||
|
||||
AnimatedBorderTextCornerRadius(
|
||||
text,
|
||||
Modifier.clickable {
|
||||
showPopup = !showPopup
|
||||
},
|
||||
)
|
||||
|
||||
if (localSecretContent != null && showPopup) {
|
||||
CoreSecretMessage(localSecretContent, callbackUri, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,4 +106,27 @@ object EmojiCoder {
|
||||
val decodedArray = ByteArray(decoded.size) { decoded[it].toByte() }
|
||||
return String(decodedArray, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun cropToFirstMessage(text: String): String {
|
||||
val decoded = mutableListOf<Int>()
|
||||
|
||||
var i = 0
|
||||
while (i < text.length) {
|
||||
val codePoint = text.codePointAt(i)
|
||||
val byte = fromVariationSelector(codePoint)
|
||||
|
||||
if (byte == null && decoded.isNotEmpty()) {
|
||||
break
|
||||
} else if (byte == null) {
|
||||
i += Character.charCount(codePoint) // Advance index by correct number of chars
|
||||
continue
|
||||
}
|
||||
|
||||
decoded.add(byte)
|
||||
i += Character.charCount(codePoint) // Advance index by correct number of chars
|
||||
}
|
||||
|
||||
return text.substring(0, i)
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ class EmojiCoderTest {
|
||||
)
|
||||
|
||||
val HELLO_WORLD = "\uD83D\uDE00\uDB40\uDD38\uDB40\uDD55\uDB40\uDD5C\uDB40\uDD5C\uDB40\uDD5F\uDB40\uDD1C\uDB40\uDD10\uDB40\uDD47\uDB40\uDD5F\uDB40\uDD62\uDB40\uDD5C\uDB40\uDD54\uDB40\uDD11"
|
||||
val HELLO_WORLD_WITH_EXTRAS = HELLO_WORLD + "askfasdf"
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -55,6 +56,11 @@ class EmojiCoderTest {
|
||||
assertEquals("Hello, World!", EmojiCoder.decode(HELLO_WORLD))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCrop() {
|
||||
assertEquals(HELLO_WORLD, EmojiCoder.cropToFirstMessage(HELLO_WORLD_WITH_EXTRAS))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncodeDecode() {
|
||||
for (emoji in EMOJI_LIST) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user