no message

This commit is contained in:
Vitor Pamplona
2025-03-05 15:28:02 -05:00
parent 91c19dc2a1
commit 2b2b219536
7 changed files with 362 additions and 48 deletions
@@ -240,6 +240,8 @@ open class NewPostViewModel : ViewModel() {
var canAddInvoice by mutableStateOf(false) var canAddInvoice by mutableStateOf(false)
var wantsInvoice by mutableStateOf(false) var wantsInvoice by mutableStateOf(false)
var wantsSecretEmoji by mutableStateOf(false)
// Forward Zap to // Forward Zap to
var wantsForwardZapTo by mutableStateOf(false) var wantsForwardZapTo by mutableStateOf(false)
var forwardZapTo by mutableStateOf<Split<User>>(Split()) var forwardZapTo by mutableStateOf<Split<User>>(Split())
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun AnimatedBorderTextCornerRadius(
text: String,
modifier: Modifier,
) {
val infiniteTransition = rememberInfiniteTransition()
val animatedFloatRestart =
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 100f,
animationSpec =
infiniteRepeatable(
animation = tween(5000, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
)
Text(
text = text,
fontSize = 12.sp,
modifier =
modifier
.drawBehind {
val brush =
Brush.sweepGradient(
colors = listOf(Color.Cyan, Color.Magenta, Color.Yellow),
)
drawRoundRect(
brush = brush,
style =
Stroke(
width = 2.dp.toPx(),
cap = StrokeCap.Round,
join = StrokeJoin.Round,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), animatedFloatRestart.value),
),
cornerRadius =
androidx.compose.ui.geometry
.CornerRadius(6.dp.toPx()),
)
}.padding(2.dp),
)
}
// Example usage in a composable function:
@Composable
@Preview
fun ExampleAnimatedBorder() {
Column {
AnimatedBorderTextCornerRadius(text = "Rounded Corners", Modifier)
}
}
@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -103,7 +104,6 @@ import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.text.Typography.paragraph
fun isMarkdown(content: String): Boolean = fun isMarkdown(content: String): Boolean =
content.startsWith("> ") || content.startsWith("> ") ||
@@ -518,7 +518,6 @@ fun DisplayFullNote(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun DisplaySecretEmoji( fun DisplaySecretEmoji(
segment: SecretEmoji, segment: SecretEmoji,
@@ -535,6 +534,10 @@ fun DisplaySecretEmoji(
mutableStateOf<RichTextViewerState?>(null) mutableStateOf<RichTextViewerState?>(null)
} }
var showPopup by remember {
mutableStateOf(false)
}
LaunchedEffect(segment) { LaunchedEffect(segment) {
launch(Dispatchers.Default) { launch(Dispatchers.Default) {
secretContent = secretContent =
@@ -545,52 +548,71 @@ fun DisplaySecretEmoji(
} }
} }
secretContent?.let { val localSecretContent = secretContent
if (it.paragraphs.size == 1) {
Text(segment.segmentText)
it.paragraphs[0].words.forEach { word ->
RenderWordWithPreview(
word,
state,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
)
}
} else if (it.paragraphs.size > 1) {
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
Column(CashuCardBorders) { AnimatedBorderTextCornerRadius(
it.paragraphs.forEach { paragraph -> segment.segmentText,
FlowRow( Modifier.clickable {
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start), showPopup = !showPopup
horizontalArrangement = Arrangement.spacedBy(spaceWidth), },
) { )
paragraph.words.forEach { word ->
RenderWordWithPreview( if (localSecretContent != null && showPopup) {
word, CoreSecretMessage(localSecretContent, callbackUri, quotesLeft, backgroundColor, accountViewModel, nav)
state,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
)
}
}
}
}
} else {
Text(segment.segmentText)
}
} }
} else { } else {
Text(segment.segmentText) Text(segment.segmentText)
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CoreSecretMessage(
localSecretContent: RichTextViewerState,
callbackUri: String?,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
if (localSecretContent.paragraphs.size == 1) {
localSecretContent.paragraphs[0].words.forEach { word ->
RenderWordWithPreview(
word,
localSecretContent,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
)
}
} else if (localSecretContent.paragraphs.size > 1) {
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
Column(CashuCardBorders) {
localSecretContent.paragraphs.forEach { paragraph ->
FlowRow(
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
) {
paragraph.words.forEach { word ->
RenderWordWithPreview(
word,
localSecretContent,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
)
}
}
}
}
}
}
@Composable @Composable
fun HashTag( fun HashTag(
segment: HashTagSegment, segment: HashTagSegment,
@@ -0,0 +1,131 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.commons.hashtags.Lightning
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun SecretEmojiRequest(onSuccess: (String) -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp),
) {
Icon(
imageVector = CustomHashTagIcons.Lightning,
null,
modifier = Size20Modifier,
tint = Color.Unspecified,
)
Text(
text = stringRes(R.string.secret_emoji_maker),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp),
)
}
HorizontalDivider(thickness = DividerThickness)
var secretMessage by remember { mutableStateOf("") }
var publicPrefix by remember { mutableStateOf("") }
OutlinedTextField(
label = { Text(text = stringRes(R.string.secret_note_to_receiver)) },
modifier = Modifier.fillMaxWidth(),
value = secretMessage,
onValueChange = { secretMessage = it },
placeholder = {
Text(
text = stringRes(R.string.secret_note_to_receiver_placeholder),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
singleLine = true,
)
OutlinedTextField(
label = { Text(text = stringRes(R.string.secret_visible_text)) },
modifier = Modifier.fillMaxWidth(),
value = publicPrefix,
onValueChange = { publicPrefix = it },
placeholder = {
Text(
text = stringRes(R.string.secret_visible_text_placeholder),
color = MaterialTheme.colorScheme.placeholderText,
)
},
singleLine = true,
)
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
onSuccess(EmojiCoder.encode(publicPrefix, secretMessage))
},
shape = QuoteBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(
text = stringRes(R.string.secret_add_to_text),
color = Color.White,
fontSize = 20.sp,
)
}
}
@@ -53,6 +53,7 @@ import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Assistant
import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.CurrencyBitcoin import androidx.compose.material.icons.filled.CurrencyBitcoin
import androidx.compose.material.icons.filled.LocationOff import androidx.compose.material.icons.filled.LocationOff
@@ -61,6 +62,7 @@ import androidx.compose.material.icons.filled.Sell
import androidx.compose.material.icons.filled.ShowChart import androidx.compose.material.icons.filled.ShowChart
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.Assistant
import androidx.compose.material.icons.rounded.Warning import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@@ -146,6 +148,7 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.ui.components.SecretEmojiRequest
import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.components.VideoView
import com.vitorpamplona.amethyst.ui.components.ZapRaiserRequest import com.vitorpamplona.amethyst.ui.components.ZapRaiserRequest
import com.vitorpamplona.amethyst.ui.navigation.Nav import com.vitorpamplona.amethyst.ui.navigation.Nav
@@ -162,7 +165,7 @@ import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.ui.note.ZapSplitIcon import com.vitorpamplona.amethyst.ui.note.ZapSplitIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatlist.utils.MyTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.MyTextField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@@ -173,6 +176,7 @@ import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size5dp
@@ -562,6 +566,20 @@ fun NewPostScreen(
} }
} }
if (postViewModel.wantsSecretEmoji) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp),
) {
Column(Modifier.fillMaxWidth()) {
SecretEmojiRequest {
postViewModel.insertAtCursor(it)
postViewModel.wantsSecretEmoji = false
}
}
}
}
if (postViewModel.wantsZapraiser && postViewModel.hasLnAddress()) { if (postViewModel.wantsZapraiser && postViewModel.hasLnAddress()) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -641,10 +659,8 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
} }
} }
if (postViewModel.canAddInvoice && postViewModel.hasLnAddress()) { ForwardZapTo(postViewModel) {
AddLnInvoiceButton(postViewModel.wantsInvoice) { postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
postViewModel.wantsInvoice = !postViewModel.wantsInvoice
}
} }
if (postViewModel.canAddZapRaiser) { if (postViewModel.canAddZapRaiser) {
@@ -661,8 +677,14 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash
} }
ForwardZapTo(postViewModel) { AddSecretEmoji(postViewModel.wantsSecretEmoji) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo postViewModel.wantsSecretEmoji = !postViewModel.wantsSecretEmoji
}
if (postViewModel.canAddInvoice && postViewModel.hasLnAddress()) {
AddLnInvoiceButton(postViewModel.wantsInvoice) {
postViewModel.wantsInvoice = !postViewModel.wantsInvoice
}
} }
} }
} }
@@ -1506,6 +1528,32 @@ private fun AddLnInvoiceButton(
} }
} }
@Composable
private fun AddSecretEmoji(
isSecretEmojiActive: Boolean,
onClick: () -> Unit,
) {
IconButton(
onClick = { onClick() },
) {
if (!isSecretEmojiActive) {
Icon(
imageVector = Icons.Outlined.Assistant,
contentDescription = stringRes(id = R.string.secret_emoji_maker_explainer),
modifier = Size20Modifier,
tint = MaterialTheme.colorScheme.onBackground,
)
} else {
Icon(
imageVector = Icons.Outlined.Assistant,
contentDescription = stringRes(id = R.string.secret_emoji_maker_explainer),
modifier = Size20Modifier,
tint = BitcoinOrange,
)
}
}
}
@Composable @Composable
private fun ForwardZapTo( private fun ForwardZapTo(
postViewModel: NewPostViewModel, postViewModel: NewPostViewModel,
+9
View File
@@ -86,6 +86,15 @@
<string name="thank_you_so_much">Thank you so much!</string> <string name="thank_you_so_much">Thank you so much!</string>
<string name="amount_in_sats">Amount in Sats</string> <string name="amount_in_sats">Amount in Sats</string>
<string name="send_sats">Send Sats</string> <string name="send_sats">Send Sats</string>
<string name="secret_emoji_maker">Secret Emoji Maker</string>
<string name="secret_emoji_maker_explainer">Add an emoji with hidden message to post</string>
<string name="secret_note_to_receiver">Secret Note to Receiver</string>
<string name="secret_note_to_receiver_placeholder">My hidden message</string>
<string name="secret_visible_text">Visible Prefix</string>
<string name="secret_visible_text_placeholder">😎</string>
<string name="secret_add_to_text">Add to Post</string>
<string name="error_parsing_preview_for">"Error parsing preview for %1$s : %2$s"</string> <string name="error_parsing_preview_for">"Error parsing preview for %1$s : %2$s"</string>
<string name="preview_card_image_for">"Preview Card Image for %1$s"</string> <string name="preview_card_image_for">"Preview Card Image for %1$s"</string>
<string name="new_channel">New Channel</string> <string name="new_channel">New Channel</string>
@@ -66,4 +66,9 @@ class EmojiCoderTest {
} }
} }
} }
@Test
fun testLinkFromAmethyst() {
assertEquals("https://cdn.satellite.earth/947e4ab2d3115be565a49cf5db02559f310ca0a6ddfddbd4bd8cbec44995c2e7.webp", EmojiCoder.decode("🚀󠅘󠅤󠅤󠅠󠅣󠄪󠄟󠄟󠅓󠅔󠅞󠄞󠅣󠅑󠅤󠅕󠅜󠅜󠅙󠅤󠅕󠄞󠅕󠅑󠅢󠅤󠅘󠄟󠄩󠄤󠄧󠅕󠄤󠅑󠅒󠄢󠅔󠄣󠄡󠄡󠄥󠅒󠅕󠄥󠄦󠄥󠅑󠄤󠄩󠅓󠅖󠄥󠅔󠅒󠄠󠄢󠄥󠄥󠄩󠅖󠄣󠄡󠄠󠅓󠅑󠄠󠅑󠄦󠅔󠅔󠅖󠅔󠅔󠅒󠅔󠄤󠅒󠅔󠄨󠅓󠅒󠅕󠅓󠄤󠄤󠄩󠄩󠄥󠅓󠄢󠅕󠄧󠄞󠅧󠅕󠅒󠅠"))
}
} }