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 {
val richTextCache = LruCache<String, RichTextViewerState>(50)
fun getCached(content: String): RichTextViewerState? = richTextCache[content]
fun parseText(
content: String,
tags: ImmutableListOfLists<String>,
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(

View File

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

View File

@ -119,6 +119,8 @@ interface EventInterface {
fun forEachTaggedEvent(onEach: (eventId: HexKey) -> Unit)
fun forEachHashTag(onEach: (eventId: HexKey) -> Unit)
fun <R> mapTaggedEvent(map: (eventId: HexKey) -> 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
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<DualCase>): 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,
)