mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
Merge pull request #2588 from vitorpamplona/claude/optimize-composables-performance-bpO05
perf(note types): cache event-derived values and hoist static modifiers
This commit is contained in:
@@ -216,7 +216,7 @@ fun RenderAppDefinition(
|
||||
CreateTextWithEmoji(
|
||||
text = it,
|
||||
tags =
|
||||
remember {
|
||||
remember(note) {
|
||||
(note.event?.tags ?: emptyArray()).toImmutableListOfLists()
|
||||
},
|
||||
fontWeight = FontWeight.Bold,
|
||||
|
||||
@@ -80,10 +80,10 @@ fun AudioTrackHeader(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val media = remember { noteEvent.media() }
|
||||
val cover = remember { noteEvent.cover() }
|
||||
val subject = remember { noteEvent.subject() }
|
||||
val participants = remember { noteEvent.participants() }
|
||||
val media = remember(noteEvent) { noteEvent.media() }
|
||||
val cover = remember(noteEvent) { noteEvent.cover() }
|
||||
val subject = remember(noteEvent) { noteEvent.subject() }
|
||||
val participants = remember(noteEvent) { noteEvent.participants() }
|
||||
|
||||
var participantUsers by remember { mutableStateOf<List<Pair<ParticipantTag, User>>>(emptyList()) }
|
||||
|
||||
@@ -183,9 +183,9 @@ fun AudioHeader(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val media = remember { noteEvent.stream() ?: noteEvent.download() }
|
||||
val waveform = remember { noteEvent.wavefrom()?.let { WaveformData(it.wave) } }
|
||||
val content = remember { noteEvent.content.ifBlank { null } }
|
||||
val media = remember(noteEvent) { noteEvent.stream() ?: noteEvent.download() }
|
||||
val waveform = remember(noteEvent) { noteEvent.wavefrom()?.let { WaveformData(it.wave) } }
|
||||
val content = remember(noteEvent) { noteEvent.content.ifBlank { null } }
|
||||
|
||||
val defaultBackground = MaterialTheme.colorScheme.background
|
||||
val background = remember { mutableStateOf(defaultBackground) }
|
||||
|
||||
@@ -51,8 +51,14 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
private val PriceTagModifier =
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.padding(start = 5.dp)
|
||||
|
||||
@Composable
|
||||
fun RenderClassifieds(
|
||||
noteEvent: ClassifiedsEvent,
|
||||
@@ -60,23 +66,31 @@ fun RenderClassifieds(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val imageSet =
|
||||
noteEvent.imageMetas().ifEmpty { null }?.map {
|
||||
MediaUrlImage(
|
||||
url = it.url,
|
||||
description = it.alt,
|
||||
hash = it.hash,
|
||||
blurhash = it.blurhash,
|
||||
dim = it.dimension,
|
||||
uri = note.toNostrUri(),
|
||||
mimeType = it.mimeType,
|
||||
thumbhash = it.thumbhash,
|
||||
)
|
||||
val imageSet: ImmutableList<MediaUrlImage>? =
|
||||
remember(noteEvent) {
|
||||
noteEvent
|
||||
.imageMetas()
|
||||
.ifEmpty { null }
|
||||
?.map {
|
||||
MediaUrlImage(
|
||||
url = it.url,
|
||||
description = it.alt,
|
||||
hash = it.hash,
|
||||
blurhash = it.blurhash,
|
||||
dim = it.dimension,
|
||||
uri = note.toNostrUri(),
|
||||
mimeType = it.mimeType,
|
||||
thumbhash = it.thumbhash,
|
||||
)
|
||||
}?.toImmutableList()
|
||||
}
|
||||
val title = noteEvent.title()
|
||||
val summary = noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null }
|
||||
val price = noteEvent.price()
|
||||
val location = noteEvent.location()
|
||||
val title = remember(noteEvent) { noteEvent.title() }
|
||||
val summary =
|
||||
remember(noteEvent) {
|
||||
noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null }
|
||||
}
|
||||
val price = remember(noteEvent) { noteEvent.price() }
|
||||
val location = remember(noteEvent) { noteEvent.location() }
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
@@ -94,7 +108,7 @@ fun RenderClassifieds(
|
||||
AutoNonlazyGrid(images.size) {
|
||||
ZoomableContentView(
|
||||
content = images[it],
|
||||
images = images.toImmutableList(),
|
||||
images = images,
|
||||
roundedCorner = false,
|
||||
contentScale = ContentScale.Crop,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -140,12 +154,7 @@ fun RenderClassifieds(
|
||||
maxLines = 1,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.padding(start = 5.dp)
|
||||
},
|
||||
modifier = PriceTagModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
|
||||
@@ -46,7 +44,7 @@ fun FileHeaderDisplay(
|
||||
val event = (note.event as? FileHeaderEvent) ?: return
|
||||
val fullUrl = event.url() ?: return
|
||||
|
||||
val content by
|
||||
val content: BaseMediaContent =
|
||||
remember(note) {
|
||||
val blurHash = event.blurhash()
|
||||
val thumbHash = event.thumbhash()
|
||||
@@ -57,32 +55,30 @@ fun FileHeaderDisplay(
|
||||
val uri = note.toNostrUri()
|
||||
val mimeType = event.mimeType()
|
||||
|
||||
mutableStateOf<BaseMediaContent>(
|
||||
if (isImage) {
|
||||
MediaUrlImage(
|
||||
url = fullUrl,
|
||||
description = description,
|
||||
hash = hash,
|
||||
blurhash = blurHash,
|
||||
dim = dimensions,
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
thumbhash = thumbHash,
|
||||
)
|
||||
} else {
|
||||
MediaUrlVideo(
|
||||
url = fullUrl,
|
||||
description = description,
|
||||
hash = hash,
|
||||
blurhash = blurHash,
|
||||
dim = dimensions,
|
||||
uri = uri,
|
||||
authorName = note.author?.toBestDisplayName(),
|
||||
mimeType = mimeType,
|
||||
thumbhash = thumbHash,
|
||||
)
|
||||
},
|
||||
)
|
||||
if (isImage) {
|
||||
MediaUrlImage(
|
||||
url = fullUrl,
|
||||
description = description,
|
||||
hash = hash,
|
||||
blurhash = blurHash,
|
||||
dim = dimensions,
|
||||
uri = uri,
|
||||
mimeType = mimeType,
|
||||
thumbhash = thumbHash,
|
||||
)
|
||||
} else {
|
||||
MediaUrlVideo(
|
||||
url = fullUrl,
|
||||
description = description,
|
||||
hash = hash,
|
||||
blurhash = blurHash,
|
||||
dim = dimensions,
|
||||
uri = uri,
|
||||
authorName = note.author?.toBestDisplayName(),
|
||||
mimeType = mimeType,
|
||||
thumbhash = thumbHash,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
||||
|
||||
@@ -357,7 +357,7 @@ fun DisplayEntryForAUrl(
|
||||
}
|
||||
|
||||
val validatedUrl =
|
||||
remember {
|
||||
remember(url) {
|
||||
try {
|
||||
URL(url)
|
||||
} catch (_: Exception) {
|
||||
|
||||
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
private const val WORDS_PER_MINUTE = 225
|
||||
private val COVER_ASPECT_RATIO = 16f / 9f
|
||||
@@ -92,7 +93,14 @@ fun LongFormHeader(
|
||||
remember(noteEvent) {
|
||||
noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null }
|
||||
}
|
||||
val topics = remember(noteEvent) { noteEvent.topics().distinct().take(3) }
|
||||
val topics =
|
||||
remember(noteEvent) {
|
||||
noteEvent
|
||||
.topics()
|
||||
.distinct()
|
||||
.take(3)
|
||||
.toImmutableList()
|
||||
}
|
||||
val readingMinutes = remember(noteEvent) { estimateReadingMinutes(noteEvent.content) }
|
||||
|
||||
Column(MaterialTheme.colorScheme.replyModifier) {
|
||||
|
||||
@@ -278,6 +278,24 @@ private fun RenderParticipants(
|
||||
}
|
||||
}
|
||||
|
||||
private val MeetingSpaceOpenModifier =
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color(0xFF4CAF50))
|
||||
.padding(horizontal = 5.dp)
|
||||
|
||||
private val MeetingSpacePrivateModifier =
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color(0xFFFF9800))
|
||||
.padding(horizontal = 5.dp)
|
||||
|
||||
private val MeetingSpaceClosedModifier =
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 5.dp)
|
||||
|
||||
@Composable
|
||||
fun MeetingSpaceOpenFlag() {
|
||||
Text(
|
||||
@@ -285,13 +303,7 @@ fun MeetingSpaceOpenFlag() {
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color(0xFF4CAF50))
|
||||
.padding(horizontal = 5.dp)
|
||||
},
|
||||
modifier = MeetingSpaceOpenModifier,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -302,13 +314,7 @@ fun MeetingSpacePrivateFlag() {
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color(0xFFFF9800))
|
||||
.padding(horizontal = 5.dp)
|
||||
},
|
||||
modifier = MeetingSpacePrivateModifier,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -319,12 +325,6 @@ fun MeetingSpaceClosedFlag() {
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
modifier =
|
||||
remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = 5.dp)
|
||||
},
|
||||
modifier = MeetingSpaceClosedModifier,
|
||||
)
|
||||
}
|
||||
|
||||
+1
-2
@@ -54,7 +54,6 @@ fun RenderNIP90ContentDiscoveryResponse(
|
||||
note = note,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
val modifier = remember(note) { Modifier.fillMaxWidth() }
|
||||
val tags =
|
||||
remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
@@ -62,7 +61,7 @@ fun RenderNIP90ContentDiscoveryResponse(
|
||||
content = noteEvent.content,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = modifier,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
|
||||
@@ -81,7 +81,7 @@ fun DisplayPeopleList(
|
||||
members.take(3)
|
||||
}
|
||||
|
||||
val name by remember { derivedStateOf { "#${noteEvent.titleOrName() ?: noteEvent.dTag()}" } }
|
||||
val name by remember(noteEvent) { derivedStateOf { "#${noteEvent.titleOrName() ?: noteEvent.dTag()}" } }
|
||||
|
||||
Text(
|
||||
text = name,
|
||||
@@ -95,7 +95,7 @@ fun DisplayPeopleList(
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(noteEvent) {
|
||||
accountViewModel.loadUsers(noteEvent.taggedUserIds()) {
|
||||
members = it
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -71,30 +69,29 @@ fun PictureDisplay(
|
||||
val isSensitive = remember(note) { event.isSensitiveOrNSFW() }
|
||||
val reasons = remember(note) { collectContentWarningReasons(event) }
|
||||
|
||||
val images by
|
||||
val images =
|
||||
remember(note) {
|
||||
mutableStateOf(
|
||||
event
|
||||
.imetaTags()
|
||||
.map {
|
||||
MediaUrlImage(
|
||||
url = it.url,
|
||||
description = it.alt,
|
||||
hash = it.hash,
|
||||
blurhash = it.blurhash,
|
||||
dim = it.dimension,
|
||||
uri = uri,
|
||||
mimeType = it.mimeType,
|
||||
thumbhash = it.thumbhash,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
event
|
||||
.imetaTags()
|
||||
.map {
|
||||
MediaUrlImage(
|
||||
url = it.url,
|
||||
description = it.alt,
|
||||
hash = it.hash,
|
||||
blurhash = it.blurhash,
|
||||
dim = it.dimension,
|
||||
uri = uri,
|
||||
mimeType = it.mimeType,
|
||||
thumbhash = it.thumbhash,
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
val first = images.firstOrNull()
|
||||
|
||||
if (first != null) {
|
||||
val title = event.title()
|
||||
val title = remember(event) { event.title() }
|
||||
val preloadUrls = remember(images) { images.map { it.url } }
|
||||
|
||||
Column {
|
||||
if (title != null) {
|
||||
@@ -114,7 +111,7 @@ fun PictureDisplay(
|
||||
ContentWarningGate(
|
||||
isSensitive = isSensitive,
|
||||
reasons = reasons,
|
||||
preloadUrls = listOf(first.url),
|
||||
preloadUrls = preloadUrls,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = mediaSizingModifier(ratio, ContentScale.FillWidth),
|
||||
backdrop = (first.thumbhash ?: first.blurhash)?.let { { BlurhashBackdrop(first.blurhash, first.description, first.thumbhash) } },
|
||||
@@ -131,7 +128,7 @@ fun PictureDisplay(
|
||||
ContentWarningGate(
|
||||
isSensitive = isSensitive,
|
||||
reasons = reasons,
|
||||
preloadUrls = images.map { it.url },
|
||||
preloadUrls = preloadUrls,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(1f),
|
||||
backdrop = { BlurhashGridBackdrop(images) },
|
||||
|
||||
@@ -65,7 +65,7 @@ fun RenderPinListEvent(
|
||||
) {
|
||||
val noteEvent = baseNote.event as? PinListEvent ?: return
|
||||
|
||||
val pins by remember { mutableStateOf(noteEvent.pinnedEvents()) }
|
||||
val pins = remember(noteEvent) { noteEvent.pinnedEvents() }
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
@@ -358,7 +358,7 @@ private fun ColumnScope.RenderSingleChoiceOptions(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val hasSpaceToClick =
|
||||
remember {
|
||||
remember(it.label) {
|
||||
it.label.contains(' ') || it.label.contains('\n')
|
||||
}
|
||||
|
||||
@@ -436,7 +436,7 @@ private fun RenderResults(
|
||||
labelContent: @Composable (ColumnScope.(code: String, label: String) -> Unit),
|
||||
) {
|
||||
val showGallery =
|
||||
remember {
|
||||
remember(card) {
|
||||
card.options.all {
|
||||
it.label.length < 50
|
||||
}
|
||||
|
||||
@@ -85,12 +85,11 @@ fun RenderPrivateMessage(
|
||||
}
|
||||
}
|
||||
|
||||
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
|
||||
val withMe = remember(noteEvent) { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
|
||||
if (withMe) {
|
||||
LoadDecryptedContent(note, accountViewModel) { eventContent ->
|
||||
val modifier = remember(note.event?.id) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser =
|
||||
remember(note.event?.id) { accountViewModel.isLoggedUser(note.author) }
|
||||
remember(note.author) { accountViewModel.isLoggedUser(note.author) }
|
||||
|
||||
val tags =
|
||||
remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
@@ -113,7 +112,7 @@ fun RenderPrivateMessage(
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
quotesLeft = quotesLeft,
|
||||
modifier = modifier,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tags = tags,
|
||||
backgroundColor = backgroundColor,
|
||||
id = note.idHex,
|
||||
|
||||
@@ -75,12 +75,10 @@ fun DisplayRelaySet(
|
||||
) {
|
||||
val noteEvent = baseNote.event as? RelaySetEvent ?: return
|
||||
|
||||
val relays by
|
||||
val relays =
|
||||
remember(noteEvent) {
|
||||
mutableStateOf(
|
||||
RelayListCard(
|
||||
noteEvent.relays().toImmutableList(),
|
||||
),
|
||||
RelayListCard(
|
||||
noteEvent.relays().toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,10 +87,12 @@ fun DisplayRelaySet(
|
||||
noteEvent.tags.firstTagValueFor("title", "name") ?: "#${noteEvent.dTag()}"
|
||||
}
|
||||
|
||||
val description = remember(noteEvent) { noteEvent.description() }
|
||||
|
||||
DisplayRelaySet(
|
||||
relays,
|
||||
relayListName,
|
||||
noteEvent.description(),
|
||||
description,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@@ -108,21 +108,17 @@ fun DisplayNIP65RelayList(
|
||||
) {
|
||||
val noteEvent = baseNote.event as? AdvertisedRelayListEvent ?: return
|
||||
|
||||
val writeRelays by
|
||||
remember(baseNote) {
|
||||
mutableStateOf(
|
||||
RelayListCard(
|
||||
noteEvent.writeRelaysNorm() ?: emptyList(),
|
||||
),
|
||||
val writeRelays =
|
||||
remember(noteEvent) {
|
||||
RelayListCard(
|
||||
noteEvent.writeRelaysNorm() ?: emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
val readRelays by
|
||||
remember(baseNote) {
|
||||
mutableStateOf(
|
||||
RelayListCard(
|
||||
noteEvent.readRelaysNorm() ?: emptyList(),
|
||||
),
|
||||
val readRelays =
|
||||
remember(noteEvent) {
|
||||
RelayListCard(
|
||||
noteEvent.readRelaysNorm() ?: emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -154,12 +150,10 @@ fun DisplayDMRelayList(
|
||||
) {
|
||||
val noteEvent = baseNote.event as? ChatMessageRelayListEvent ?: return
|
||||
|
||||
val relays by
|
||||
remember(baseNote) {
|
||||
mutableStateOf(
|
||||
RelayListCard(
|
||||
noteEvent.relays(),
|
||||
),
|
||||
val relays =
|
||||
remember(noteEvent) {
|
||||
RelayListCard(
|
||||
noteEvent.relays(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -48,35 +48,43 @@ fun RenderReport(
|
||||
) {
|
||||
val noteEvent = note.event as? ReportEvent ?: return
|
||||
|
||||
val base = remember { (noteEvent.reportedPost() + noteEvent.reportedAuthor()) }
|
||||
val reportTypes =
|
||||
remember(noteEvent) {
|
||||
(noteEvent.reportedPost() + noteEvent.reportedAuthor())
|
||||
.mapTo(LinkedHashSet()) { it.type }
|
||||
}
|
||||
|
||||
val reportType =
|
||||
base
|
||||
.map {
|
||||
when (it.type) {
|
||||
ReportType.EXPLICIT -> stringRes(R.string.explicit_content)
|
||||
ReportType.NUDITY -> stringRes(R.string.nudity)
|
||||
ReportType.PROFANITY -> stringRes(R.string.profanity_hateful_speech)
|
||||
ReportType.SPAM -> stringRes(R.string.spam)
|
||||
ReportType.IMPERSONATION -> stringRes(R.string.impersonation)
|
||||
ReportType.ILLEGAL -> stringRes(R.string.illegal_behavior)
|
||||
ReportType.MALWARE -> stringRes(R.string.malware)
|
||||
ReportType.OTHER -> stringRes(R.string.other)
|
||||
ReportType.HARASSMENT -> stringRes(R.string.harassment)
|
||||
ReportType.VIOLENCE -> stringRes(R.string.violence)
|
||||
null -> stringRes(R.string.other)
|
||||
}
|
||||
}.toSet()
|
||||
.joinToString(", ")
|
||||
val explicitContent = stringRes(R.string.explicit_content)
|
||||
val nudity = stringRes(R.string.nudity)
|
||||
val profanity = stringRes(R.string.profanity_hateful_speech)
|
||||
val spam = stringRes(R.string.spam)
|
||||
val impersonation = stringRes(R.string.impersonation)
|
||||
val illegal = stringRes(R.string.illegal_behavior)
|
||||
val malware = stringRes(R.string.malware)
|
||||
val other = stringRes(R.string.other)
|
||||
val harassment = stringRes(R.string.harassment)
|
||||
val violence = stringRes(R.string.violence)
|
||||
|
||||
val content =
|
||||
remember {
|
||||
reportType + (
|
||||
note.event
|
||||
?.content
|
||||
?.ifBlank { null }
|
||||
?.let { ": $it" } ?: ""
|
||||
)
|
||||
remember(reportTypes, noteEvent) {
|
||||
val reportTypeText =
|
||||
reportTypes.joinToString(", ") {
|
||||
when (it) {
|
||||
ReportType.EXPLICIT -> explicitContent
|
||||
ReportType.NUDITY -> nudity
|
||||
ReportType.PROFANITY -> profanity
|
||||
ReportType.SPAM -> spam
|
||||
ReportType.IMPERSONATION -> impersonation
|
||||
ReportType.ILLEGAL -> illegal
|
||||
ReportType.MALWARE -> malware
|
||||
ReportType.OTHER -> other
|
||||
ReportType.HARASSMENT -> harassment
|
||||
ReportType.VIOLENCE -> violence
|
||||
null -> other
|
||||
}
|
||||
}
|
||||
val extra = noteEvent.content.ifBlank { null }?.let { ": $it" } ?: ""
|
||||
reportTypeText + extra
|
||||
}
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
|
||||
@@ -153,19 +153,24 @@ fun RenderTorrent(
|
||||
) {
|
||||
val noteEvent = note.event as? TorrentEvent ?: return
|
||||
|
||||
val name = (noteEvent.title() ?: TorrentEvent.ALT_DESCRIPTION)
|
||||
val size = " (" + countToHumanReadableBytes(noteEvent.totalSizeBytes()) + ")"
|
||||
|
||||
val description =
|
||||
if (noteEvent.content != name) {
|
||||
noteEvent.content
|
||||
} else {
|
||||
null
|
||||
val title =
|
||||
remember(noteEvent) {
|
||||
val name = noteEvent.title() ?: TorrentEvent.ALT_DESCRIPTION
|
||||
val size = " (" + countToHumanReadableBytes(noteEvent.totalSizeBytes()) + ")"
|
||||
name + size
|
||||
}
|
||||
|
||||
val description =
|
||||
remember(noteEvent) {
|
||||
val name = noteEvent.title() ?: TorrentEvent.ALT_DESCRIPTION
|
||||
if (noteEvent.content != name) noteEvent.content else null
|
||||
}
|
||||
|
||||
val files = remember(noteEvent) { noteEvent.files().toImmutableList() }
|
||||
|
||||
DisplayFileList(
|
||||
noteEvent.files().toImmutableList(),
|
||||
name + size,
|
||||
files,
|
||||
title,
|
||||
description,
|
||||
noteEvent::toMagnetLink,
|
||||
backgroundColor,
|
||||
|
||||
@@ -30,8 +30,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -81,45 +79,43 @@ fun VideoDisplay(
|
||||
|
||||
val imeta = videoEvent.imetaTags().firstOrNull() ?: return
|
||||
|
||||
val title = videoEvent.title()
|
||||
val summary = videoEvent.content.ifBlank { null }?.takeIf { title != it }
|
||||
val image = imeta.image.firstOrNull()
|
||||
val isYouTube = imeta.url.contains("youtube.com") || imeta.url.contains("youtu.be")
|
||||
val title = remember(videoEvent) { videoEvent.title() }
|
||||
val summary = remember(videoEvent, title) { videoEvent.content.ifBlank { null }?.takeIf { title != it } }
|
||||
val image = remember(imeta) { imeta.image.firstOrNull() }
|
||||
val isYouTube = remember(imeta) { imeta.url.contains("youtube.com") || imeta.url.contains("youtu.be") }
|
||||
val tags = remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
val content by
|
||||
val content: BaseMediaContent =
|
||||
remember(note) {
|
||||
val description = videoEvent.content.ifBlank { null } ?: event.alt()
|
||||
val isImage = imeta.mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(imeta.url)
|
||||
val uri = note.toNostrUri()
|
||||
|
||||
mutableStateOf<BaseMediaContent>(
|
||||
if (isImage) {
|
||||
MediaUrlImage(
|
||||
url = imeta.url,
|
||||
description = description,
|
||||
hash = imeta.hash,
|
||||
blurhash = imeta.blurhash,
|
||||
dim = imeta.dimension,
|
||||
uri = uri,
|
||||
mimeType = imeta.mimeType,
|
||||
thumbhash = imeta.thumbhash,
|
||||
)
|
||||
} else {
|
||||
MediaUrlVideo(
|
||||
url = imeta.url,
|
||||
description = description,
|
||||
hash = imeta.hash,
|
||||
dim = imeta.dimension,
|
||||
uri = uri,
|
||||
authorName = note.author?.toBestDisplayName(),
|
||||
artworkUri = imeta.image.firstOrNull(),
|
||||
mimeType = imeta.mimeType,
|
||||
blurhash = imeta.blurhash,
|
||||
thumbhash = imeta.thumbhash,
|
||||
)
|
||||
},
|
||||
)
|
||||
if (isImage) {
|
||||
MediaUrlImage(
|
||||
url = imeta.url,
|
||||
description = description,
|
||||
hash = imeta.hash,
|
||||
blurhash = imeta.blurhash,
|
||||
dim = imeta.dimension,
|
||||
uri = uri,
|
||||
mimeType = imeta.mimeType,
|
||||
thumbhash = imeta.thumbhash,
|
||||
)
|
||||
} else {
|
||||
MediaUrlVideo(
|
||||
url = imeta.url,
|
||||
description = description,
|
||||
hash = imeta.hash,
|
||||
dim = imeta.dimension,
|
||||
uri = uri,
|
||||
authorName = note.author?.toBestDisplayName(),
|
||||
artworkUri = imeta.image.firstOrNull(),
|
||||
mimeType = imeta.mimeType,
|
||||
blurhash = imeta.blurhash,
|
||||
thumbhash = imeta.thumbhash,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
||||
|
||||
+78
-4
@@ -25,14 +25,16 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
@@ -45,6 +47,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChatroomHeaderCompose
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
|
||||
@Composable
|
||||
fun ChatroomListFeedView(
|
||||
@@ -103,14 +113,16 @@ private fun FeedLoaded(
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
|
||||
val myPubKey = accountViewModel.userProfile().pubkeyHex
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items(
|
||||
items.list,
|
||||
key = { index, item -> if (index == 0) index else item.idHex },
|
||||
) { _, item ->
|
||||
key = { item -> chatroomLazyKey(item, myPubKey) },
|
||||
) { item ->
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
ChatroomHeaderCompose(
|
||||
item,
|
||||
@@ -125,3 +137,65 @@ private fun FeedLoaded(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stable per-chatroom key — derived from chatroom identity, not the latest
|
||||
// message id, so reorders move the row instead of recreating it. Uses a
|
||||
// sealed wrapper around an existing String/RoomId/ChatroomKey to avoid the
|
||||
// StringBuilder + concatenation allocations of a typed-prefix string key.
|
||||
private sealed interface ChatroomLazyKey
|
||||
|
||||
private data class MarmotChatroomLazyKey(
|
||||
val groupId: HexKey,
|
||||
) : ChatroomLazyKey
|
||||
|
||||
private data class PublicChannelLazyKey(
|
||||
val channelId: HexKey,
|
||||
) : ChatroomLazyKey
|
||||
|
||||
private data class EphemeralChannelLazyKey(
|
||||
val roomId: RoomId,
|
||||
) : ChatroomLazyKey
|
||||
|
||||
private data class PrivateChatLazyKey(
|
||||
val key: ChatroomKey,
|
||||
) : ChatroomLazyKey
|
||||
|
||||
private data class FallbackChatroomLazyKey(
|
||||
val noteIdHex: HexKey,
|
||||
) : ChatroomLazyKey
|
||||
|
||||
private fun chatroomLazyKey(
|
||||
item: Note,
|
||||
myPubKey: HexKey,
|
||||
): ChatroomLazyKey {
|
||||
item.inGatherers
|
||||
?.firstNotNullOfOrNull { it as? MarmotGroupChatroom }
|
||||
?.let { return MarmotChatroomLazyKey(it.nostrGroupId) }
|
||||
|
||||
return when (val event = item.event) {
|
||||
is ChannelMessageEvent -> {
|
||||
PublicChannelLazyKey(event.channelId() ?: item.idHex)
|
||||
}
|
||||
|
||||
is ChannelMetadataEvent -> {
|
||||
PublicChannelLazyKey(event.channelId() ?: item.idHex)
|
||||
}
|
||||
|
||||
is ChannelCreateEvent -> {
|
||||
PublicChannelLazyKey(event.id)
|
||||
}
|
||||
|
||||
is EphemeralChatEvent -> {
|
||||
event.roomId()?.let { EphemeralChannelLazyKey(it) }
|
||||
?: FallbackChatroomLazyKey(item.idHex)
|
||||
}
|
||||
|
||||
is ChatroomKeyable -> {
|
||||
PrivateChatLazyKey(event.chatroomKey(myPubKey))
|
||||
}
|
||||
|
||||
else -> {
|
||||
FallbackChatroomLazyKey(item.idHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user