diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 59a189ba6..01249e1b8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -275,9 +275,10 @@ private fun RenderUserAsClickableText( CreateClickableTextWithEmoji( clickablePart = it, suffix = addedCharts, - tags = userTags, + maxLines = 1, route = route, - nav = nav + nav = nav, + tags = userTags ) } } @@ -286,24 +287,38 @@ private fun RenderUserAsClickableText( fun CreateClickableText( clickablePart: String, suffix: String, + maxLines: Int = Int.MAX_VALUE, overrideColor: Color? = null, fontWeight: FontWeight = FontWeight.Normal, route: String, nav: (String) -> Unit ) { - ClickableText( - text = buildAnnotatedString { - withStyle( - LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle() - ) { + val currentStyle = LocalTextStyle.current + val primaryColor = MaterialTheme.colors.primary + val onBackgroundColor = MaterialTheme.colors.onBackground + + val clickablePartStyle = remember(primaryColor, overrideColor) { + currentStyle.copy(color = overrideColor ?: primaryColor, fontWeight = fontWeight).toSpanStyle() + } + + val nonClickablePartStyle = remember(onBackgroundColor, overrideColor) { + currentStyle.copy(color = overrideColor ?: onBackgroundColor, fontWeight = fontWeight).toSpanStyle() + } + + val text = remember(clickablePartStyle, nonClickablePartStyle, clickablePart, suffix) { + buildAnnotatedString { + withStyle(clickablePartStyle) { append(clickablePart) } - withStyle( - LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle() - ) { + withStyle(nonClickablePartStyle) { append(suffix) } - }, + } + } + + ClickableText( + text = text, + maxLines = maxLines, onClick = { nav(route) } ) } @@ -410,14 +425,17 @@ fun CreateTextWithEmoji( modifier = modifier ) } else { - val style = LocalTextStyle.current.merge( - TextStyle( - color = textColor, - textAlign = textAlign, - fontWeight = fontWeight, - fontSize = fontSize - ) - ).toSpanStyle() + val currentStyle = LocalTextStyle.current + val style = remember(currentStyle) { + currentStyle.merge( + TextStyle( + color = textColor, + textAlign = textAlign, + fontWeight = fontWeight, + fontSize = fontSize + ) + ).toSpanStyle() + } InLineIconRenderer(emojiList, style, maxLines, overflow, modifier) } @@ -426,6 +444,7 @@ fun CreateTextWithEmoji( @Composable fun CreateClickableTextWithEmoji( clickablePart: String, + maxLines: Int = Int.MAX_VALUE, tags: ImmutableListOfLists?, style: TextStyle, onClick: (Int) -> Unit @@ -448,29 +467,37 @@ fun CreateClickableTextWithEmoji( if (emojiList.isEmpty()) { ClickableText( - AnnotatedString(clickablePart), + text = AnnotatedString(clickablePart), style = style, + maxLines = maxLines, onClick = onClick ) } else { - ClickableInLineIconRenderer(emojiList, style.toSpanStyle()) { + ClickableInLineIconRenderer(emojiList, maxLines, style.toSpanStyle()) { onClick(it) } } } +@Immutable +data class DoubleEmojiList( + val part1: ImmutableList, + val part2: ImmutableList +) + @Composable fun CreateClickableTextWithEmoji( clickablePart: String, suffix: String, - tags: ImmutableListOfLists?, + maxLines: Int = Int.MAX_VALUE, overrideColor: Color? = null, fontWeight: FontWeight = FontWeight.Normal, route: String, - nav: (String) -> Unit + nav: (String) -> Unit, + tags: ImmutableListOfLists? ) { var emojiLists by remember(clickablePart) { - mutableStateOf, ImmutableList>?>(null) + mutableStateOf(null) } LaunchedEffect(key1 = clickablePart) { @@ -483,20 +510,28 @@ fun CreateClickableTextWithEmoji( val newEmojiList2 = assembleAnnotatedList(suffix, emojis) if (newEmojiList1.isNotEmpty() || newEmojiList2.isNotEmpty()) { - emojiLists = Pair(newEmojiList1.toImmutableList(), newEmojiList2.toImmutableList()) + emojiLists = DoubleEmojiList(newEmojiList1.toImmutableList(), newEmojiList2.toImmutableList()) } } } } if (emojiLists == null) { - CreateClickableText(clickablePart, suffix, overrideColor, fontWeight, route, nav) + CreateClickableText(clickablePart, suffix, maxLines, overrideColor, fontWeight, route, nav) } else { - ClickableInLineIconRenderer(emojiLists!!.first, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle()) { + ClickableInLineIconRenderer( + emojiLists!!.part1, + maxLines, + LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.primary, fontWeight = fontWeight).toSpanStyle() + ) { nav(route) } - InLineIconRenderer(emojiLists!!.second, LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle()) + InLineIconRenderer( + emojiLists!!.part2, + LocalTextStyle.current.copy(color = overrideColor ?: MaterialTheme.colors.onBackground, fontWeight = fontWeight).toSpanStyle(), + maxLines + ) } } @@ -521,7 +556,12 @@ class TextType(val text: String) : Renderable() class ImageUrlType(val url: String) : Renderable() @Composable -fun ClickableInLineIconRenderer(wordsInOrder: ImmutableList, style: SpanStyle, onClick: (Int) -> Unit) { +fun ClickableInLineIconRenderer( + wordsInOrder: ImmutableList, + maxLines: Int = Int.MAX_VALUE, + style: SpanStyle, + onClick: (Int) -> Unit +) { val inlineContent = wordsInOrder.mapIndexedNotNull { idx, value -> if (value is ImageUrlType) { Pair( @@ -574,6 +614,7 @@ fun ClickableInLineIconRenderer(wordsInOrder: ImmutableList, style: text = annotatedText, modifier = pressIndicator, inlineContent = inlineContent, + maxLines = maxLines, onTextLayout = { layoutResult.value = it } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 0f14b8e08..34f2b8ac2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -1041,8 +1041,9 @@ private fun DisplayUserFromTag( CreateClickableTextWithEmoji( clickablePart = displayName, suffix = remember { "$addedChars " }, - tags = userTags, + maxLines = 1, route = route, - nav = nav + nav = nav, + tags = userTags ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index f05abfc04..9d59c5fba 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent @@ -72,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.ui.theme.Size13dp import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size16dp +import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink @@ -261,11 +263,11 @@ fun NormalChatNote( } } - LaunchedEffect(key1 = routeForLastRead) { - routeForLastRead?.let { + if (routeForLastRead != null) { + LaunchedEffect(key1 = routeForLastRead) { val createdAt = note.createdAt() if (createdAt != null) { - accountViewModel.account.markAsRead(it, createdAt) + accountViewModel.account.markAsRead(routeForLastRead, createdAt) } } } @@ -369,43 +371,72 @@ private fun RenderBubble( } Column(modifier = bubbleModifier) { - if (drawAuthorInfo) { - DrawAuthorInfo( - baseNote, - alignment, - nav - ) - } else { - Spacer(modifier = StdVertSpacer) - } + MessageBubbleLines( + drawAuthorInfo, + baseNote, + alignment, + nav, + innerQuote, + backgroundBubbleColor, + accountViewModel, + onWantsToReply, + canPreview, + bubbleSize, + availableBubbleSize + ) + } +} - RenderReplyRow( - note = baseNote, - innerQuote = innerQuote, - backgroundBubbleColor = backgroundBubbleColor, +@Composable +private fun MessageBubbleLines( + drawAuthorInfo: Boolean, + baseNote: Note, + alignment: Arrangement.Horizontal, + nav: (String) -> Unit, + innerQuote: Boolean, + backgroundBubbleColor: MutableState, + accountViewModel: AccountViewModel, + onWantsToReply: (Note) -> Unit, + canPreview: Boolean, + bubbleSize: MutableState, + availableBubbleSize: MutableState +) { + if (drawAuthorInfo) { + DrawAuthorInfo( + baseNote, + alignment, + nav + ) + } else { + Spacer(modifier = StdVertSpacer) + } + + RenderReplyRow( + note = baseNote, + innerQuote = innerQuote, + backgroundBubbleColor = backgroundBubbleColor, + accountViewModel = accountViewModel, + nav = nav, + onWantsToReply = onWantsToReply + ) + + NoteRow( + note = baseNote, + canPreview = canPreview, + backgroundBubbleColor = backgroundBubbleColor, + accountViewModel = accountViewModel, + nav = nav + ) + + ConstrainedStatusRow( + bubbleSize = bubbleSize, + availableBubbleSize = availableBubbleSize + ) { + StatusRow( + baseNote = baseNote, accountViewModel = accountViewModel, - nav = nav, onWantsToReply = onWantsToReply ) - - NoteRow( - note = baseNote, - canPreview = canPreview, - backgroundBubbleColor = backgroundBubbleColor, - accountViewModel = accountViewModel, - nav = nav - ) - - ConstrainedStatusRow( - bubbleSize = bubbleSize, - availableBubbleSize = availableBubbleSize - ) { - StatusRow( - baseNote = baseNote, - accountViewModel = accountViewModel, - onWantsToReply = onWantsToReply - ) - } } } @@ -418,25 +449,42 @@ private fun RenderReplyRow( nav: (String) -> Unit, onWantsToReply: (Note) -> Unit ) { - val replyTo by remember { + val hasReply by remember { derivedStateOf { - note.replyTo?.lastOrNull() + innerQuote && note.replyTo?.lastOrNull() != null } } - if (!innerQuote && replyTo != null) { - Row(verticalAlignment = Alignment.CenterVertically) { - replyTo?.let { note -> - ChatroomMessageCompose( - note, - null, - innerQuote = true, - parentBackgroundColor = backgroundBubbleColor, - accountViewModel = accountViewModel, - nav = nav, - onWantsToReply = onWantsToReply - ) + + if (hasReply) { + RenderReply(note, backgroundBubbleColor, accountViewModel, nav, onWantsToReply) + } +} + +@Composable +private fun RenderReply( + note: Note, + backgroundBubbleColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, + onWantsToReply: (Note) -> Unit +) { + Row(verticalAlignment = Alignment.CenterVertically) { + val replyTo by remember { + derivedStateOf { + note.replyTo?.lastOrNull() } } + replyTo?.let { note -> + ChatroomMessageCompose( + note, + null, + innerQuote = true, + parentBackgroundColor = backgroundBubbleColor, + accountViewModel = accountViewModel, + nav = nav, + onWantsToReply = onWantsToReply + ) + } } } @@ -501,9 +549,7 @@ private fun StatusRow( accountViewModel: AccountViewModel, onWantsToReply: (Note) -> Unit ) { - val grayTint = MaterialTheme.colors.placeholderText - - Column() { + Column { Row(verticalAlignment = Alignment.CenterVertically) { ChatTimeAgo(baseNote) RelayBadges(baseNote) @@ -511,16 +557,16 @@ private fun StatusRow( } } - Column() { + Column { Row(verticalAlignment = Alignment.CenterVertically) { - LikeReaction(baseNote, grayTint, accountViewModel) + LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel) Spacer(modifier = StdHorzSpacer) - ZapReaction(baseNote, grayTint, accountViewModel) + ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel) Spacer(modifier = StdHorzSpacer) ReplyReaction( - baseNote, - grayTint, - accountViewModel, + baseNote = baseNote, + grayTint = MaterialTheme.colors.placeholderText, + accountViewModel = accountViewModel, showCounter = false, iconSize = Size16dp ) { @@ -644,51 +690,107 @@ private fun DrawAuthorInfo( alignment: Arrangement.Horizontal, nav: (String) -> Unit ) { - val userState by baseNote.author!!.live().metadata.observeAsState() - - val pubkeyHex = remember { baseNote.author?.pubkeyHex } ?: return - val route = remember { "User/$pubkeyHex" } - val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() } - val userProfilePicture = remember(userState) { ResizeImage(userState?.user?.profilePicture(), 25.dp) } - val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() } - Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = alignment, modifier = Modifier.padding(top = 5.dp) ) { - RobohashAsyncImageProxy( - robot = pubkeyHex, - model = userProfilePicture, - contentDescription = stringResource(id = R.string.profile_image), - modifier = remember { - Modifier - .width(25.dp) - .height(25.dp) - .clip(shape = CircleShape) - .clickable(onClick = { - nav(route) - }) - } - ) + DisplayAndWatchNoteAuthor(baseNote, nav) + } +} - userDisplayName?.let { - Spacer(modifier = StdHorzSpacer) +@Composable +private fun DisplayAndWatchNoteAuthor( + baseNote: Note, + nav: (String) -> Unit +) { + val author = remember { + baseNote.author + } + author?.let { + WatchAndDisplayUser(it, nav) + } +} - CreateClickableTextWithEmoji( - clickablePart = it, - suffix = "", - tags = userTags, - fontWeight = FontWeight.Bold, - overrideColor = MaterialTheme.colors.onBackground, - route = route, - nav = nav - ) +@Composable +private fun WatchAndDisplayUser( + author: User, + nav: (String) -> Unit +) { + val pubkeyHex = remember { author.pubkeyHex } + val route = remember { "User/${author.pubkeyHex}" } - Spacer(modifier = StdHorzSpacer) - DrawPlayName(it) + val userState by author.live().metadata.observeAsState() + + val userDisplayName by remember(userState) { + derivedStateOf { + userState?.user?.toBestDisplayName() } } + + val userProfilePicture by remember(userState) { + derivedStateOf { + ResizeImage(userState?.user?.profilePicture(), Size25dp) + } + } + + val userTags by remember(userState) { + derivedStateOf { + userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() + } + } + + UserIcon(pubkeyHex, userProfilePicture, nav, route) + + userDisplayName?.let { + DisplayMessageUsername(it, userTags, route, nav) + } +} + +@Composable +private fun UserIcon( + pubkeyHex: String, + userProfilePicture: ResizeImage, + nav: (String) -> Unit, + route: String +) { + RobohashAsyncImageProxy( + robot = pubkeyHex, + model = userProfilePicture, + contentDescription = stringResource(id = R.string.profile_image), + modifier = remember { + Modifier + .width(Size25dp) + .height(Size25dp) + .clip(shape = CircleShape) + .clickable(onClick = { + nav(route) + }) + } + ) +} + +@Composable +private fun DisplayMessageUsername( + userDisplayName: String, + userTags: ImmutableListOfLists?, + route: String, + nav: (String) -> Unit +) { + Spacer(modifier = StdHorzSpacer) + CreateClickableTextWithEmoji( + clickablePart = userDisplayName, + suffix = "", + maxLines = 1, + tags = userTags, + fontWeight = FontWeight.Bold, + overrideColor = MaterialTheme.colors.onBackground, // we do not want clickable names in purple here. + route = route, + nav = nav + ) + + Spacer(modifier = StdHorzSpacer) + DrawPlayName(userDisplayName) } @Immutable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index b0221a267..2ce53d58e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -2062,9 +2062,10 @@ private fun LoadAndDisplayUser( CreateClickableTextWithEmoji( clickablePart = userDisplayName, suffix = " ", - tags = userTags, + maxLines = 1, route = route, - nav = nav + nav = nav, + tags = userTags ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index ca27cb033..3a2bf2322 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -65,14 +65,16 @@ private fun UserNameDisplay( fontWeight = FontWeight.Bold, maxLines = 1 ) - CreateTextWithEmoji( - text = remember { "@$bestUserName" }, - tags = tags, - color = MaterialTheme.colors.placeholderText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = modifier - ) + if (bestDisplayName != bestUserName) { + CreateTextWithEmoji( + text = remember { "@$bestUserName" }, + tags = tags, + color = MaterialTheme.colors.placeholderText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier + ) + } Spacer(StdHorzSpacer) DrawPlayName(bestDisplayName) } else if (bestDisplayName != null) { @@ -88,7 +90,7 @@ private fun UserNameDisplay( DrawPlayName(bestDisplayName) } else if (bestUserName != null) { CreateTextWithEmoji( - text = remember { "@$bestUserName" }, + text = bestUserName, tags = tags, fontWeight = FontWeight.Bold, maxLines = 1, @@ -99,7 +101,7 @@ private fun UserNameDisplay( DrawPlayName(bestUserName) } else { Text( - npubDisplay, + text = npubDisplay, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -113,8 +115,15 @@ fun DrawPlayName(name: String) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current + DrawPlayNameIcon { + speak(name, context, lifecycleOwner) + } +} + +@Composable +fun DrawPlayNameIcon(onClick: () -> Unit) { IconButton( - onClick = { speak(name, context, lifecycleOwner) }, + onClick = onClick, modifier = StdButtonSizeModifier ) { Icon( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index a0e3cda4d..f8c613de8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -99,6 +99,7 @@ import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.SmallBorder import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer @@ -236,7 +237,7 @@ fun ChannelScreen( ) } - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = DoubleVertSpacer) replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index c3af22b4e..18581e910 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -26,10 +26,12 @@ val StdButtonSizeModifier = Modifier.size(20.dp) val StdHorzSpacer = Modifier.width(5.dp) val StdVertSpacer = Modifier.height(5.dp) val DoubleHorzSpacer = Modifier.width(10.dp) +val DoubleVertSpacer = Modifier.width(10.dp) -val Size35dp = 35.dp val Size13dp = 13.dp val Size16dp = 16.dp +val Size25dp = 25.dp +val Size35dp = 35.dp val StdPadding = Modifier.padding(10.dp)