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 8ebaea37b..ad51fd2b2 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 @@ -143,27 +143,46 @@ private fun RenderRegular( } } + val paragraphs = remember(content) { + content.split('\n').toImmutableList() + } + // FlowRow doesn't work well with paragraphs. So we need to split them - content.split('\n').forEach { paragraph -> - FlowRow() { - val s = remember(paragraph) { - if (isArabic(paragraph)) { - paragraph.trim().split(' ').reversed() - } else { - paragraph.trim().split(' ') - } - } - s.forEach { word: String -> - RenderWord( - word, - state, - canPreview, - backgroundColor, - accountViewModel, - nav, - tags - ) - } + paragraphs.forEach { paragraph -> + RenderParagraph(paragraph, state, canPreview, backgroundColor, accountViewModel, nav, tags) + } +} + +@Composable +@OptIn(ExperimentalLayoutApi::class) +private fun RenderParagraph( + paragraph: String, + state: RichTextViewerState, + canPreview: Boolean, + backgroundColor: Color, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, + tags: ImmutableListOfLists +) { + val s = remember(paragraph) { + if (isArabic(paragraph)) { + paragraph.trim().split(' ').reversed().toImmutableList() + } else { + paragraph.trim().split(' ').toImmutableList() + } + } + + FlowRow() { + s.forEach { word: String -> + RenderWord( + word, + state, + canPreview, + backgroundColor, + accountViewModel, + nav, + tags + ) } } } @@ -863,13 +882,14 @@ fun HashTag(word: String, nav: (String) -> Unit) { } ?: Text(text = "$word ") } +data class LoadedTag(val user: User?, val note: Note?, val addedChars: String) + @Composable fun TagLink(word: String, tags: ImmutableListOfLists, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - var baseUserPair by remember { mutableStateOf?>(null) } - var baseNotePair by remember { mutableStateOf?>(null) } + var loadedTag by remember { mutableStateOf(null) } LaunchedEffect(key1 = word) { - if (baseUserPair == null && baseNotePair == null) { + if (loadedTag == null) { launch(Dispatchers.IO) { val matcher = tagIndex.matcher(word) val (index, suffix) = try { @@ -877,7 +897,7 @@ fun TagLink(word: String, tags: ImmutableListOfLists, canPreview: Boolea Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "") } catch (e: Exception) { Log.w("Tag Parser", "Couldn't link tag $word", e) - Pair(null, null) + Pair(null, "") } if (index != null && index >= 0 && index < tags.lists.size) { @@ -886,11 +906,11 @@ fun TagLink(word: String, tags: ImmutableListOfLists, canPreview: Boolea if (tag.size > 1) { if (tag[0] == "p") { LocalCache.checkGetOrCreateUser(tag[1])?.let { - baseUserPair = Pair(it, suffix) + loadedTag = LoadedTag(it, null, suffix) } } else if (tag[0] == "e" || tag[0] == "a") { LocalCache.checkGetOrCreateNote(tag[1])?.let { - baseNotePair = Pair(it, suffix) + loadedTag = LoadedTag(null, it, suffix) } } } @@ -899,55 +919,80 @@ fun TagLink(word: String, tags: ImmutableListOfLists, canPreview: Boolea } } - baseUserPair?.let { - val innerUserState by it.first.live().metadata.observeAsState() - val displayName = remember(innerUserState) { - innerUserState?.user?.toBestDisplayName() ?: "" - } - val route = remember(innerUserState) { - "User/${it.first.pubkeyHex}" - } - val userTags = remember(innerUserState) { - innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() - } - - CreateClickableTextWithEmoji( - clickablePart = displayName, - suffix = remember { "${it.second} " }, - tags = userTags, - route = route, - nav = nav - ) - } - - baseNotePair?.let { - if (canPreview) { - NoteCompose( - baseNote = it.first, - accountViewModel = accountViewModel, - modifier = Modifier - .padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp) - .fillMaxWidth() - .clip(shape = RoundedCornerShape(15.dp)) - .border( - 1.dp, - MaterialTheme.colors.onSurface.copy(alpha = 0.12f), - RoundedCornerShape(15.dp) - ), - parentBackgroundColor = backgroundColor, - isQuotedNote = true, - nav = nav - ) - it.second?.ifBlank { null }?.let { - Text(text = "$it ") + if (loadedTag == null) { + Text( + text = remember { + "$word " } - } else { - ClickableNoteTag(it.first, nav) - Text(text = "${it.second} ") + ) + } else { + loadedTag?.user?.let { + DisplayUserFromTag(it, loadedTag?.addedChars ?: "", nav) } - } - if (baseNotePair == null && baseUserPair == null) { - Text(text = "$word ") + loadedTag?.note?.let { + DisplayNoteFromTag(it, loadedTag?.addedChars ?: "", canPreview, accountViewModel, backgroundColor, nav) + } } } + +@Composable +private fun DisplayNoteFromTag( + baseNote: Note, + addedChars: String, + canPreview: Boolean, + accountViewModel: AccountViewModel, + backgroundColor: Color, + nav: (String) -> Unit +) { + if (canPreview) { + NoteCompose( + baseNote = baseNote, + accountViewModel = accountViewModel, + modifier = Modifier + .padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ), + parentBackgroundColor = backgroundColor, + isQuotedNote = true, + nav = nav + ) + addedChars.ifBlank { null }?.let { + Text(text = remember { "$it " }) + } + } else { + ClickableNoteTag(baseNote, nav) + Text(text = remember { "$addedChars " }) + } +} + +@Composable +private fun DisplayUserFromTag( + baseUser: User, + addedChars: String, + nav: (String) -> Unit +) { + val innerUserState by baseUser.live().metadata.observeAsState() + val displayName = remember(innerUserState) { + innerUserState?.user?.toBestDisplayName() ?: "" + } + val route = remember(innerUserState) { + "User/${baseUser.pubkeyHex}" + } + val userTags = remember(innerUserState) { + innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() + } + + CreateClickableTextWithEmoji( + clickablePart = displayName, + suffix = remember { "$addedChars " }, + tags = userTags, + route = route, + nav = nav + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt index 5c6f09ed2..e07d8475f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -39,7 +39,7 @@ abstract class AdditiveFeedFilter : FeedFilter() { } } - Log.d("Time", "${this.javaClass.simpleName} Additive Feed in $elapsed with ${feed.size} objects") + // Log.d("Time", "${this.javaClass.simpleName} Additive Feed in $elapsed with ${feed.size} objects") return feed } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index c7f6786c2..6034c9c00 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -65,12 +65,10 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun val baseNote = remember { multiSetCard.note } var popupExpanded by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() - var isNew by remember { mutableStateOf(false) } - LaunchedEffect(key1 = multiSetCard.createdAt()) { + LaunchedEffect(key1 = multiSetCard) { launch(Dispatchers.IO) { val newIsNew = multiSetCard.maxCreatedAt > NotificationCache.load(routeForLastRead) @@ -85,10 +83,14 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun val primaryColor = MaterialTheme.colors.newItemBackgroundColor val defaultBackgroundColor = MaterialTheme.colors.background - val backgroundColor = if (isNew) { - primaryColor.compositeOver(defaultBackgroundColor) - } else { - defaultBackgroundColor + val backgroundColor by remember(isNew) { + derivedStateOf { + if (isNew) { + primaryColor.compositeOver(defaultBackgroundColor) + } else { + defaultBackgroundColor + } + } } val columnModifier = Modifier @@ -332,7 +334,7 @@ private fun AuthorPictureAndComment( Box(modifier = remember { Modifier.size(35.dp) }, contentAlignment = Alignment.BottomCenter) { FastNoteAuthorPicture( author = author, - size = 35.dp, + size = remember { 35.dp }, accountViewModel = accountViewModel, pictureModifier = authorPictureModifier ) @@ -380,10 +382,10 @@ fun AuthorGallery( nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - Column(modifier = Modifier.padding(start = 10.dp)) { + Column(modifier = remember { Modifier.padding(start = 10.dp) }) { FlowRow() { authorNotes.forEach { note -> - Box(Modifier.size(35.dp)) { + Box(remember { Modifier.size(35.dp) }) { NotePictureAndComment(note, backgroundColor, nav, accountViewModel) } } 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 7d05a8138..da78213a5 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 @@ -154,7 +154,6 @@ import java.io.File import java.math.BigDecimal import java.net.URL import java.util.Locale -import kotlin.time.ExperimentalTime @OptIn(ExperimentalFoundationApi::class) @Composable @@ -331,20 +330,21 @@ private fun WatchForReports( onChange: (Boolean, Boolean, Set) -> Unit ) { val accountState by accountViewModel.accountLiveData.observeAsState() - val account = remember(accountState) { accountState?.account } ?: return - val noteReportsState by note.live().reports.observeAsState() - val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return LaunchedEffect(key1 = noteReportsState, key2 = accountState) { launch(Dispatchers.Default) { - account.userProfile().let { loggedIn -> - val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex || - (note.author?.let { loggedIn.isFollowingCached(it) } ?: true) || - !(noteForReports.hasAnyReports()) + accountState?.account?.let { loggedIn -> + val newCanPreview = note.author?.pubkeyHex == loggedIn.userProfile().pubkeyHex || + (note.author?.let { loggedIn.userProfile().isFollowingCached(it) } ?: true) || + noteReportsState?.note?.hasAnyReports() != true - val newIsAcceptable = account.isAcceptable(noteForReports) - val newRelevantReports = account.getRelevantReports(noteForReports) + val newIsAcceptable = noteReportsState?.note?.let { + loggedIn.isAcceptable(it) + } ?: true + val newRelevantReports = noteReportsState?.note?.let { + loggedIn.getRelevantReports(it) + } ?: emptySet() onChange(newIsAcceptable, newCanPreview, newRelevantReports) } @@ -1489,11 +1489,11 @@ private fun FirstUserInfoRow( Row(verticalAlignment = Alignment.CenterVertically) { if (showAuthorPicture) { - NoteAuthorPicture(baseNote, nav, accountViewModel, 25.dp) + NoteAuthorPicture(baseNote, nav, accountViewModel, remember { 25.dp }) Spacer(padding) - NoteUsernameDisplay(baseNote, Modifier.weight(1f)) + NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) }) } else { - NoteUsernameDisplay(baseNote, Modifier.weight(1f)) + NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) }) } if (eventNote is RepostEvent) { @@ -1560,7 +1560,6 @@ fun TimeAgo(time: Long) { ) } -@OptIn(ExperimentalTime::class) @Composable private fun DrawAuthorImages(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val baseChannelHex = remember { baseNote.channelHex() } @@ -1670,7 +1669,7 @@ private fun RepostNoteAuthorPicture( baseNote = it, nav = nav, accountViewModel = accountViewModel, - size = 30.dp, + size = remember { 30.dp }, pictureModifier = Modifier.border( 2.dp, MaterialTheme.colors.background, @@ -2511,6 +2510,8 @@ fun UserPicture( .clip(shape = CircleShape) } + val myIconSize = remember(size) { size.div(3.5f) } + Box(myBoxModifier, contentAlignment = TopEnd) { RobohashAsyncImageProxy( robot = userHex, @@ -2521,7 +2522,7 @@ fun UserPicture( modifier = myImageModifier.background(MaterialTheme.colors.background) ) - ObserveAndDisplayFollowingMark(userHex, size.div(3.5f), accountViewModel) + ObserveAndDisplayFollowingMark(userHex, myIconSize, accountViewModel) } }