Faster calculation of uncited hashtags.

This commit is contained in:
Vitor Pamplona
2024-06-17 17:22:39 -04:00
parent 0fa6dce42c
commit 5d5c3ae3e3
14 changed files with 115 additions and 131 deletions

View File

@@ -28,13 +28,16 @@ import com.vitorpamplona.quartz.events.ImmutableListOfLists
object CachedRichTextParser { object CachedRichTextParser {
val richTextCache = LruCache<String, RichTextViewerState>(50) val richTextCache = LruCache<String, RichTextViewerState>(50)
fun getCached(content: String): RichTextViewerState? = richTextCache[content]
fun parseText( fun parseText(
content: String, content: String,
tags: ImmutableListOfLists<String>, tags: ImmutableListOfLists<String>,
callbackUri: String? = null, callbackUri: String? = null,
): RichTextViewerState { ): RichTextViewerState {
return if (richTextCache[content] != null) { val cached = richTextCache[content]
richTextCache[content] return if (cached != null) {
cached
} else { } else {
val newUrls = RichTextParser().parseText(content, tags, callbackUri) val newUrls = RichTextParser().parseText(content, tags, callbackUri)
richTextCache.put(content, newUrls) richTextCache.put(content, newUrls)

View File

@@ -26,23 +26,75 @@ import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.text.AnnotatedString 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.HalfTopPadding
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink 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) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun DisplayUncitedHashtags( fun DisplayUncitedHashtags(
hashtags: ImmutableList<String>, event: Event,
eventContent: String, content: String,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val unusedHashtags by
produceState(initialValue = emptyList<String>()) {
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 = val unusedHashtags =
remember(eventContent) { hashtags.filter { !eventContent.contains(it, true) } } tagsInEvent.filterNot { eventTag ->
tagsInContent.any { contentTag ->
eventTag.equals(contentTag, true)
}
}
if (unusedHashtags.isNotEmpty()) { if (unusedHashtags.isNotEmpty()) {
value = unusedHashtags
}
}
}
if (unusedHashtags.isNotEmpty()) {
val style =
LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.lessImportantLink,
)
FlowRow( FlowRow(
modifier = HalfTopPadding, modifier = HalfTopPadding,
) { ) {
@@ -50,10 +102,7 @@ fun DisplayUncitedHashtags(
ClickableText( ClickableText(
text = remember { AnnotatedString("#$hashtag ") }, text = remember { AnnotatedString("#$hashtag ") },
onClick = { nav("Hashtag/$hashtag") }, onClick = { nav("Hashtag/$hashtag") },
style = style = style,
LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.lessImportantLink,
),
) )
} }
} }

View File

@@ -240,8 +240,7 @@ fun AudioHeader(
if (noteEvent.hasHashtags()) { if (noteEvent.hasHashtags()) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
val hashtags = remember(noteEvent) { noteEvent.hashtags().toImmutableList() } DisplayUncitedHashtags(noteEvent, nav)
DisplayUncitedHashtags(hashtags, content ?: "", nav)
} }
} }
} }

View File

@@ -79,7 +79,6 @@ import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.Participant import com.vitorpamplona.quartz.events.Participant
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.util.Locale import java.util.Locale
@Composable @Composable
@@ -90,7 +89,8 @@ fun RenderCommunity(
) { ) {
if (baseNote is AddressableNote) { if (baseNote is AddressableNote) {
Row( Row(
MaterialTheme.colorScheme.innerPostModifier.clickable { MaterialTheme.colorScheme.innerPostModifier
.clickable {
routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) } routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) }
}.padding(Size10dp), }.padding(Size10dp),
) { ) {
@@ -160,8 +160,8 @@ fun LongCommunityHeader(
if (summary != null && noteEvent.hasHashtags()) { if (summary != null && noteEvent.hasHashtags()) {
DisplayUncitedHashtags( DisplayUncitedHashtags(
hashtags = remember(key1 = noteEvent) { noteEvent.hashtags().toImmutableList() }, event = noteEvent,
eventContent = summary, content = summary,
nav = nav, nav = nav,
) )
} }
@@ -361,8 +361,7 @@ fun WatchAddressableNoteFollows(
accountViewModel.userFollows accountViewModel.userFollows
.map { it.user.latestContactList?.isTaggedAddressableNote(note.idHex) ?: false } .map { it.user.latestContactList?.isTaggedAddressableNote(note.idHex) ?: false }
.distinctUntilChanged() .distinctUntilChanged()
} }.observeAsState(false)
.observeAsState(false)
onFollowChanges(showFollowingMark) onFollowChanges(showFollowingMark)
} }

View File

@@ -66,8 +66,6 @@ import com.vitorpamplona.quartz.events.GitPatchEvent
import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.GitRepositoryEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun RenderGitPatchEvent( fun RenderGitPatchEvent(
@@ -195,11 +193,11 @@ private fun RenderGitPatchEvent(
} }
if (note.event?.hasHashtags() == true) { if (note.event?.hasHashtags() == true) {
val hashtags = DisplayUncitedHashtags(
remember(note.event) { event = noteEvent,
note.event?.hashtags()?.toImmutableList() ?: persistentListOf() content = eventContent,
} nav = nav,
DisplayUncitedHashtags(hashtags, eventContent, nav) )
} }
} }
} }
@@ -299,11 +297,7 @@ private fun RenderGitIssueEvent(
} }
if (note.event?.hasHashtags() == true) { if (note.event?.hasHashtags() == true) {
val hashtags = DisplayUncitedHashtags(noteEvent, eventContent, nav)
remember(note.event) {
note.event?.hashtags()?.toImmutableList() ?: persistentListOf()
}
DisplayUncitedHashtags(hashtags, eventContent, nav)
} }
} }
} }

View File

@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.ui.note.types package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState 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.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent 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.note.elements.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.EmptyTagList
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun RenderNIP90ContentDiscoveryResponse( fun RenderNIP90ContentDiscoveryResponse(
@@ -59,39 +53,7 @@ fun RenderNIP90ContentDiscoveryResponse(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val noteEvent = note.event val noteEvent = note.event as? Event ?: return
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)
}
}
LoadDecryptedContent( LoadDecryptedContent(
note, note,
@@ -118,9 +80,6 @@ fun RenderNIP90ContentDiscoveryResponse(
} }
} }
val isAuthorTheLoggedUser =
remember(note.event) { accountViewModel.isLoggedUser(note.author) }
SensitivityWarning( SensitivityWarning(
note = note, note = note,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
@@ -143,12 +102,8 @@ fun RenderNIP90ContentDiscoveryResponse(
) )
} }
if (note.event?.hasHashtags() == true) { if (noteEvent.hasHashtags()) {
val hashtags = DisplayUncitedHashtags(noteEvent, eventContent, nav)
remember(note.event) {
note.event?.hashtags()?.toImmutableList() ?: persistentListOf()
}
DisplayUncitedHashtags(hashtags, eventContent, nav)
} }
} }
} }

View File

@@ -45,7 +45,6 @@ import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun RenderPoll( fun RenderPoll(
@@ -126,8 +125,7 @@ fun RenderPoll(
} }
if (noteEvent.hasHashtags()) { if (noteEvent.hasHashtags()) {
val hashtags = remember { noteEvent.hashtags().toImmutableList() } DisplayUncitedHashtags(noteEvent, eventContent, nav)
DisplayUncitedHashtags(hashtags, eventContent, nav)
} }
} }
} }

View File

@@ -52,8 +52,6 @@ import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun RenderPrivateMessage( fun RenderPrivateMessage(
@@ -122,11 +120,7 @@ fun RenderPrivateMessage(
} }
if (noteEvent.hasHashtags()) { if (noteEvent.hasHashtags()) {
val hashtags = DisplayUncitedHashtags(noteEvent, eventContent, nav)
remember(note.event?.id()) {
note.event?.hashtags()?.toImmutableList() ?: persistentListOf()
}
DisplayUncitedHashtags(hashtags, eventContent, nav)
} }
} }
} }

View File

@@ -46,10 +46,9 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.BaseTextNoteEvent import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun RenderTextEvent( fun RenderTextEvent(
@@ -63,7 +62,7 @@ fun RenderTextEvent(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val noteEvent = note.event val noteEvent = note.event as? Event ?: return
val showReply by val showReply by
remember(note) { remember(note) {
@@ -137,7 +136,6 @@ fun RenderTextEvent(
note = note, note = note,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) { ) {
val modifier = remember(note) { Modifier.fillMaxWidth() }
val tags = val tags =
remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
@@ -145,7 +143,7 @@ fun RenderTextEvent(
content = eventContent, content = eventContent,
canPreview = canPreview && !makeItShort, canPreview = canPreview && !makeItShort,
quotesLeft = quotesLeft, quotesLeft = quotesLeft,
modifier = modifier, modifier = Modifier.fillMaxWidth(),
tags = tags, tags = tags,
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
id = note.idHex, id = note.idHex,
@@ -155,12 +153,8 @@ fun RenderTextEvent(
) )
} }
if (note.event?.hasHashtags() == true) { if (noteEvent.hasHashtags()) {
val hashtags = DisplayUncitedHashtags(noteEvent, eventContent, nav)
remember(note.event) {
note.event?.hashtags()?.toImmutableList() ?: persistentListOf()
}
DisplayUncitedHashtags(hashtags, eventContent, nav)
} }
} }
} }

View File

@@ -58,7 +58,6 @@ import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun VideoDisplay( fun VideoDisplay(
@@ -178,17 +177,13 @@ fun VideoDisplay(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
}
if (event.hasHashtags()) { if (event.hasHashtags()) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
) { ) {
DisplayUncitedHashtags( DisplayUncitedHashtags(event, summary, nav)
remember(event) { event.hashtags().toImmutableList() }, }
summary ?: "",
nav,
)
} }
} }
} }

View File

@@ -710,8 +710,7 @@ fun ShowVideoStreaming(
.map { .map {
val activity = it.channel as? LiveActivitiesChannel val activity = it.channel as? LiveActivitiesChannel
activity?.info activity?.info
} }.distinctUntilChanged()
.distinctUntilChanged()
.observeAsState(baseChannel.info) .observeAsState(baseChannel.info)
streamingInfo?.let { event -> streamingInfo?.let { event ->
@@ -869,12 +868,12 @@ fun LongChannelHeader(
) )
} }
if (baseChannel is LiveActivitiesChannel && baseChannel.info?.hasHashtags() == true) { if (baseChannel is LiveActivitiesChannel && summary != null) {
val hashtags = baseChannel.info?.let {
remember(baseChannel.info) { if (it.hasHashtags()) {
baseChannel.info?.hashtags()?.toImmutableList() ?: persistentListOf() DisplayUncitedHashtags(it, summary, nav)
}
} }
DisplayUncitedHashtags(hashtags, summary ?: "", nav)
} }
} }
@@ -938,8 +937,7 @@ fun LongChannelHeader(
?.participants() ?.participants()
?.mapNotNull { part -> ?.mapNotNull { part ->
LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) } LocalCache.checkGetOrCreateUser(part.key)?.let { Pair(part, it) }
} }?.toImmutableList()
?.toImmutableList()
if ( if (
newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers) newParticipantUsers != null && !equalImmutableLists(newParticipantUsers, participantUsers)
@@ -1155,7 +1153,8 @@ fun OfflineFlag() {
fun ScheduledFlag(starts: Long?) { fun ScheduledFlag(starts: Long?) {
val startsIn = val startsIn =
starts?.let { starts?.let {
SimpleDateFormat.getDateTimeInstance( SimpleDateFormat
.getDateTimeInstance(
DateFormat.SHORT, DateFormat.SHORT,
DateFormat.SHORT, DateFormat.SHORT,
).format(Date(starts * 1000)) ).format(Date(starts * 1000))

View File

@@ -89,6 +89,8 @@ open class Event(
override fun forEachTaggedEvent(onEach: (eventId: HexKey) -> Unit) = forEachTagged("e", onEach) override fun forEachTaggedEvent(onEach: (eventId: HexKey) -> Unit) = forEachTagged("e", onEach)
override fun forEachHashTag(onEach: (eventId: HexKey) -> Unit) = forEachTagged("t", onEach)
private fun forEachTagged( private fun forEachTagged(
tagName: String, tagName: String,
onEach: (eventId: HexKey) -> Unit, onEach: (eventId: HexKey) -> Unit,

View File

@@ -119,6 +119,8 @@ interface EventInterface {
fun forEachTaggedEvent(onEach: (eventId: HexKey) -> Unit) fun forEachTaggedEvent(onEach: (eventId: HexKey) -> Unit)
fun forEachHashTag(onEach: (eventId: HexKey) -> Unit)
fun <R> mapTaggedEvent(map: (eventId: HexKey) -> R): List<R> fun <R> mapTaggedEvent(map: (eventId: HexKey) -> R): List<R>
fun <R> mapTaggedAddress(map: (address: String) -> R): List<R> fun <R> mapTaggedAddress(map: (address: String) -> R): List<R>

View File

@@ -22,9 +22,7 @@ package com.vitorpamplona.quartz.utils
import kotlin.math.min import kotlin.math.min
fun String.bytesUsedInMemory(): Int { fun String.bytesUsedInMemory(): Int = (8 * ((((this.length) * 2) + 45) / 8))
return (8 * ((((this.length) * 2) + 45) / 8))
}
fun String.containsIgnoreCase(term: String): Boolean { fun String.containsIgnoreCase(term: String): Boolean {
if (term.isEmpty()) return true // Empty string is contained if (term.isEmpty()) return true // Empty string is contained
@@ -72,4 +70,7 @@ fun String.containsAny(terms: List<DualCase>): Boolean {
return terms.any { containsIgnoreCase(it.lowercase, it.uppercase) } return terms.any { containsIgnoreCase(it.lowercase, it.uppercase) }
} }
class DualCase(val lowercase: String, val uppercase: String) class DualCase(
val lowercase: String,
val uppercase: String,
)