diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index cd1140444..8a7027a66 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -28,13 +28,16 @@ import com.vitorpamplona.quartz.events.ImmutableListOfLists object CachedRichTextParser { val richTextCache = LruCache(50) + fun getCached(content: String): RichTextViewerState? = richTextCache[content] + fun parseText( content: String, tags: ImmutableListOfLists, callbackUri: String? = null, ): RichTextViewerState { - return if (richTextCache[content] != null) { - richTextCache[content] + val cached = richTextCache[content] + return if (cached != null) { + cached } else { val newUrls = RichTextParser().parseText(content, tags, callbackUri) richTextCache.put(content, newUrls) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt index 551f60689..97c48e96f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt @@ -26,23 +26,75 @@ import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.text.AnnotatedString +import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment +import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.ui.theme.lessImportantLink -import kotlinx.collections.immutable.ImmutableList +import com.vitorpamplona.quartz.events.Event +import com.vitorpamplona.quartz.events.toImmutableListOfLists +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun DisplayUncitedHashtags( + event: Event, + nav: (String) -> Unit, +) { + DisplayUncitedHashtags(event, event.content, nav) +} @OptIn(ExperimentalLayoutApi::class) @Composable fun DisplayUncitedHashtags( - hashtags: ImmutableList, - eventContent: String, + event: Event, + content: String, nav: (String) -> Unit, ) { - val unusedHashtags = - remember(eventContent) { hashtags.filter { !eventContent.contains(it, true) } } + val unusedHashtags by + produceState(initialValue = emptyList()) { + withContext(Dispatchers.Default) { + val state = CachedRichTextParser.parseText(content, event.tags.toImmutableListOfLists()) + + val tagsInEvent = event.hashtags() + + if (tagsInEvent.isEmpty()) return@withContext + + val tagsInContent = + state + .paragraphs + .map { + it.words.mapNotNull { + if (it is HashTagSegment) { + it.hashtag + } else { + null + } + } + }.flatten() + + val unusedHashtags = + tagsInEvent.filterNot { eventTag -> + tagsInContent.any { contentTag -> + eventTag.equals(contentTag, true) + } + } + + if (unusedHashtags.isNotEmpty()) { + value = unusedHashtags + } + } + } if (unusedHashtags.isNotEmpty()) { + val style = + LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.lessImportantLink, + ) + FlowRow( modifier = HalfTopPadding, ) { @@ -50,10 +102,7 @@ fun DisplayUncitedHashtags( ClickableText( text = remember { AnnotatedString("#$hashtag ") }, onClick = { nav("Hashtag/$hashtag") }, - style = - LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.lessImportantLink, - ), + style = style, ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index 1c59001dc..7b3215b96 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -240,8 +240,7 @@ fun AudioHeader( if (noteEvent.hasHashtags()) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - val hashtags = remember(noteEvent) { noteEvent.hashtags().toImmutableList() } - DisplayUncitedHashtags(hashtags, content ?: "", nav) + DisplayUncitedHashtags(noteEvent, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 5386e101d..3477c8af2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -79,7 +79,6 @@ import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.Participant import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import java.util.Locale @Composable @@ -90,9 +89,10 @@ fun RenderCommunity( ) { if (baseNote is AddressableNote) { Row( - MaterialTheme.colorScheme.innerPostModifier.clickable { - routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) } - }.padding(Size10dp), + MaterialTheme.colorScheme.innerPostModifier + .clickable { + routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) } + }.padding(Size10dp), ) { ShortCommunityHeader( baseNote = baseNote, @@ -160,8 +160,8 @@ fun LongCommunityHeader( if (summary != null && noteEvent.hasHashtags()) { DisplayUncitedHashtags( - hashtags = remember(key1 = noteEvent) { noteEvent.hashtags().toImmutableList() }, - eventContent = summary, + event = noteEvent, + content = summary, nav = nav, ) } @@ -361,8 +361,7 @@ fun WatchAddressableNoteFollows( accountViewModel.userFollows .map { it.user.latestContactList?.isTaggedAddressableNote(note.idHex) ?: false } .distinctUntilChanged() - } - .observeAsState(false) + }.observeAsState(false) onFollowChanges(showFollowingMark) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index 27b8f7681..e4a0a6b62 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -66,8 +66,6 @@ import com.vitorpamplona.quartz.events.GitPatchEvent import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList @Composable fun RenderGitPatchEvent( @@ -195,11 +193,11 @@ private fun RenderGitPatchEvent( } if (note.event?.hasHashtags() == true) { - val hashtags = - remember(note.event) { - note.event?.hashtags()?.toImmutableList() ?: persistentListOf() - } - DisplayUncitedHashtags(hashtags, eventContent, nav) + DisplayUncitedHashtags( + event = noteEvent, + content = eventContent, + nav = nav, + ) } } } @@ -299,11 +297,7 @@ private fun RenderGitIssueEvent( } if (note.event?.hasHashtags() == true) { - val hashtags = - remember(note.event) { - note.event?.hashtags()?.toImmutableList() ?: persistentListOf() - } - DisplayUncitedHashtags(hashtags, eventContent, nav) + DisplayUncitedHashtags(noteEvent, eventContent, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt index a4d6f6812..02a060d57 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.types -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -35,17 +34,12 @@ import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent -import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import com.vitorpamplona.quartz.events.BaseTextNoteEvent -import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList @Composable fun RenderNIP90ContentDiscoveryResponse( @@ -59,39 +53,7 @@ fun RenderNIP90ContentDiscoveryResponse( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val noteEvent = note.event - - val showReply by - remember(note) { - derivedStateOf { - noteEvent is BaseTextNoteEvent && !makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser()) - } - } - - if (showReply) { - val replyingDirectlyTo = - remember(note) { - if (noteEvent is BaseTextNoteEvent) { - val replyingTo = noteEvent.replyingToAddressOrEvent() - if (replyingTo != null) { - val newNote = accountViewModel.getNoteIfExists(replyingTo) - if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) { - newNote - } else { - note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND } - } - } else { - note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND } - } - } else { - note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND } - } - } - if (replyingDirectlyTo != null && unPackReply) { - ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav) - Spacer(modifier = StdVertSpacer) - } - } + val noteEvent = note.event as? Event ?: return LoadDecryptedContent( note, @@ -118,9 +80,6 @@ fun RenderNIP90ContentDiscoveryResponse( } } - val isAuthorTheLoggedUser = - remember(note.event) { accountViewModel.isLoggedUser(note.author) } - SensitivityWarning( note = note, accountViewModel = accountViewModel, @@ -143,12 +102,8 @@ fun RenderNIP90ContentDiscoveryResponse( ) } - if (note.event?.hasHashtags() == true) { - val hashtags = - remember(note.event) { - note.event?.hashtags()?.toImmutableList() ?: persistentListOf() - } - DisplayUncitedHashtags(hashtags, eventContent, nav) + if (noteEvent.hasHashtags()) { + DisplayUncitedHashtags(noteEvent, eventContent, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index 27db72f57..eedd9003f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -45,7 +45,6 @@ import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists -import kotlinx.collections.immutable.toImmutableList @Composable fun RenderPoll( @@ -126,8 +125,7 @@ fun RenderPoll( } if (noteEvent.hasHashtags()) { - val hashtags = remember { noteEvent.hashtags().toImmutableList() } - DisplayUncitedHashtags(hashtags, eventContent, nav) + DisplayUncitedHashtags(noteEvent, eventContent, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index ba026b5ca..69cf512f7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -52,8 +52,6 @@ import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList @Composable fun RenderPrivateMessage( @@ -122,11 +120,7 @@ fun RenderPrivateMessage( } if (noteEvent.hasHashtags()) { - val hashtags = - remember(note.event?.id()) { - note.event?.hashtags()?.toImmutableList() ?: persistentListOf() - } - DisplayUncitedHashtags(hashtags, eventContent, nav) + DisplayUncitedHashtags(noteEvent, eventContent, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 775b1f7ab..e98dcec4c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -46,10 +46,9 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.BaseTextNoteEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList @Composable fun RenderTextEvent( @@ -63,7 +62,7 @@ fun RenderTextEvent( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val noteEvent = note.event + val noteEvent = note.event as? Event ?: return val showReply by remember(note) { @@ -137,7 +136,6 @@ fun RenderTextEvent( note = note, accountViewModel = accountViewModel, ) { - val modifier = remember(note) { Modifier.fillMaxWidth() } val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } @@ -145,7 +143,7 @@ fun RenderTextEvent( content = eventContent, canPreview = canPreview && !makeItShort, quotesLeft = quotesLeft, - modifier = modifier, + modifier = Modifier.fillMaxWidth(), tags = tags, backgroundColor = backgroundColor, id = note.idHex, @@ -155,12 +153,8 @@ fun RenderTextEvent( ) } - if (note.event?.hasHashtags() == true) { - val hashtags = - remember(note.event) { - note.event?.hashtags()?.toImmutableList() ?: persistentListOf() - } - DisplayUncitedHashtags(hashtags, eventContent, nav) + if (noteEvent.hasHashtags()) { + DisplayUncitedHashtags(noteEvent, eventContent, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index 18636729a..79f1ef696 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -58,7 +58,6 @@ import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists -import kotlinx.collections.immutable.toImmutableList @Composable fun VideoDisplay( @@ -178,17 +177,13 @@ fun VideoDisplay( accountViewModel = accountViewModel, nav = nav, ) - } - if (event.hasHashtags()) { - Row( - Modifier.fillMaxWidth(), - ) { - DisplayUncitedHashtags( - remember(event) { event.hashtags().toImmutableList() }, - summary ?: "", - nav, - ) + if (event.hasHashtags()) { + Row( + Modifier.fillMaxWidth(), + ) { + DisplayUncitedHashtags(event, summary, nav) + } } } } 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 7648f7244..1f886feb8 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 @@ -710,8 +710,7 @@ fun ShowVideoStreaming( .map { val activity = it.channel as? LiveActivitiesChannel activity?.info - } - .distinctUntilChanged() + }.distinctUntilChanged() .observeAsState(baseChannel.info) streamingInfo?.let { event -> @@ -869,12 +868,12 @@ fun LongChannelHeader( ) } - if (baseChannel is LiveActivitiesChannel && baseChannel.info?.hasHashtags() == true) { - val hashtags = - remember(baseChannel.info) { - baseChannel.info?.hashtags()?.toImmutableList() ?: persistentListOf() + if (baseChannel is LiveActivitiesChannel && summary != null) { + baseChannel.info?.let { + if (it.hasHashtags()) { + DisplayUncitedHashtags(it, summary, nav) } - DisplayUncitedHashtags(hashtags, summary ?: "", nav) + } } } @@ -938,8 +937,7 @@ fun LongChannelHeader( ?.participants() ?.mapNotNull { part -> LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) } - } - ?.toImmutableList() + }?.toImmutableList() if ( newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers) @@ -1155,10 +1153,11 @@ fun OfflineFlag() { fun ScheduledFlag(starts: Long?) { val startsIn = starts?.let { - SimpleDateFormat.getDateTimeInstance( - DateFormat.SHORT, - DateFormat.SHORT, - ).format(Date(starts * 1000)) + SimpleDateFormat + .getDateTimeInstance( + DateFormat.SHORT, + DateFormat.SHORT, + ).format(Date(starts * 1000)) } Text( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index 5051d9386..6228b7461 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -89,6 +89,8 @@ open class Event( override fun forEachTaggedEvent(onEach: (eventId: HexKey) -> Unit) = forEachTagged("e", onEach) + override fun forEachHashTag(onEach: (eventId: HexKey) -> Unit) = forEachTagged("t", onEach) + private fun forEachTagged( tagName: String, onEach: (eventId: HexKey) -> Unit, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt index 957ff7a99..5465a50f3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -119,6 +119,8 @@ interface EventInterface { fun forEachTaggedEvent(onEach: (eventId: HexKey) -> Unit) + fun forEachHashTag(onEach: (eventId: HexKey) -> Unit) + fun mapTaggedEvent(map: (eventId: HexKey) -> R): List fun mapTaggedAddress(map: (address: String) -> R): List diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt index 0682eccfb..305d487be 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/StringUtils.kt @@ -22,9 +22,7 @@ package com.vitorpamplona.quartz.utils import kotlin.math.min -fun String.bytesUsedInMemory(): Int { - return (8 * ((((this.length) * 2) + 45) / 8)) -} +fun String.bytesUsedInMemory(): Int = (8 * ((((this.length) * 2) + 45) / 8)) fun String.containsIgnoreCase(term: String): Boolean { if (term.isEmpty()) return true // Empty string is contained @@ -72,4 +70,7 @@ fun String.containsAny(terms: List): Boolean { return terms.any { containsIgnoreCase(it.lowercase, it.uppercase) } } -class DualCase(val lowercase: String, val uppercase: String) +class DualCase( + val lowercase: String, + val uppercase: String, +)