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

View File

@ -240,6 +240,8 @@ open class NewPostViewModel : ViewModel() {
var canAddInvoice by mutableStateOf(false)
var wantsInvoice by mutableStateOf(false)
var wantsSecretEmoji by mutableStateOf(false)
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
var forwardZapTo by mutableStateOf<Split<User>>(Split())

View File

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

View File

@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -103,7 +104,6 @@ import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.text.Typography.paragraph
fun isMarkdown(content: String): Boolean =
content.startsWith("> ") ||
@ -518,7 +518,6 @@ fun DisplayFullNote(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplaySecretEmoji(
segment: SecretEmoji,
@ -535,6 +534,10 @@ fun DisplaySecretEmoji(
mutableStateOf<RichTextViewerState?>(null)
}
var showPopup by remember {
mutableStateOf(false)
}
LaunchedEffect(segment) {
launch(Dispatchers.Default) {
secretContent =
@ -545,52 +548,71 @@ fun DisplaySecretEmoji(
}
}
secretContent?.let {
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)
val localSecretContent = secretContent
Column(CashuCardBorders) {
it.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,
state,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
)
}
}
}
}
} else {
Text(segment.segmentText)
}
AnimatedBorderTextCornerRadius(
segment.segmentText,
Modifier.clickable {
showPopup = !showPopup
},
)
if (localSecretContent != null && showPopup) {
CoreSecretMessage(localSecretContent, callbackUri, quotesLeft, backgroundColor, accountViewModel, nav)
}
} else {
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
fun HashTag(
segment: HashTagSegment,

View File

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

View File

@ -53,6 +53,7 @@ import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
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.CurrencyBitcoin
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.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.Assistant
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Button
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.LoadUrlPreview
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.ZapRaiserRequest
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.WatchAndLoadMyEmojiList
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.stringRes
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.Size10dp
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.Size55dp
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()) {
Row(
verticalAlignment = Alignment.CenterVertically,
@ -641,10 +659,8 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
}
}
if (postViewModel.canAddInvoice && postViewModel.hasLnAddress()) {
AddLnInvoiceButton(postViewModel.wantsInvoice) {
postViewModel.wantsInvoice = !postViewModel.wantsInvoice
}
ForwardZapTo(postViewModel) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
}
if (postViewModel.canAddZapRaiser) {
@ -661,8 +677,14 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash
}
ForwardZapTo(postViewModel) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
AddSecretEmoji(postViewModel.wantsSecretEmoji) {
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
private fun ForwardZapTo(
postViewModel: NewPostViewModel,

View File

@ -86,6 +86,15 @@
<string name="thank_you_so_much">Thank you so much!</string>
<string name="amount_in_sats">Amount in 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="preview_card_image_for">"Preview Card Image for %1$s"</string>
<string name="new_channel">New Channel</string>

View File

@ -66,4 +66,9 @@ class EmojiCoderTest {
}
}
}
@Test
fun testLinkFromAmethyst() {
assertEquals("https://cdn.satellite.earth/947e4ab2d3115be565a49cf5db02559f310ca0a6ddfddbd4bd8cbec44995c2e7.webp", EmojiCoder.decode("🚀󠅘󠅤󠅤󠅠󠅣󠄪󠄟󠄟󠅓󠅔󠅞󠄞󠅣󠅑󠅤󠅕󠅜󠅜󠅙󠅤󠅕󠄞󠅕󠅑󠅢󠅤󠅘󠄟󠄩󠄤󠄧󠅕󠄤󠅑󠅒󠄢󠅔󠄣󠄡󠄡󠄥󠅒󠅕󠄥󠄦󠄥󠅑󠄤󠄩󠅓󠅖󠄥󠅔󠅒󠄠󠄢󠄥󠄥󠄩󠅖󠄣󠄡󠄠󠅓󠅑󠄠󠅑󠄦󠅔󠅔󠅖󠅔󠅔󠅒󠅔󠄤󠅒󠅔󠄨󠅓󠅒󠅕󠅓󠄤󠄤󠄩󠄩󠄥󠅓󠄢󠅕󠄧󠄞󠅧󠅕󠅒󠅠"))
}
}