Skip to content

Commit

Permalink
Adding secret emojis to reactions
Browse files Browse the repository at this point in the history
  • Loading branch information
vitorpamplona committed Mar 5, 2025
1 parent 8722f37 commit 9cd73c2
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -125,5 +126,9 @@ fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists<String>): String {
}
}

if (EmojiCoder.isCoded(this)) {
return EmojiCoder.cropToFirstMessage(this)
}

return firstFullChar()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -61,7 +66,7 @@ fun AnimatedBorderTextCornerRadius(

Text(
text = text,
fontSize = 12.sp,
fontSize = fontSize,
modifier =
modifier
.drawBehind {
Expand All @@ -84,6 +89,8 @@ fun AnimatedBorderTextCornerRadius(
.CornerRadius(6.dp.toPx()),
)
}.padding(3.dp),
color = color,
textAlign = textAlign,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
}
}
}
}
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
}
}
Expand Down Expand Up @@ -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 = {},
Expand Down Expand Up @@ -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,
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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("")
Expand Down Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading

0 comments on commit 9cd73c2

Please sign in to comment.