From cec204b7ae1bf625cd923298cdc8fe9320d4ddb8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 2 Oct 2023 17:43:38 -0400 Subject: [PATCH] Breaks down Compose components in the Discovery tab. --- .../amethyst/ui/note/ChannelCardCompose.kt | 482 ++++++++++-------- 1 file changed, 272 insertions(+), 210 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index 338cddc9e..6b849f8ab 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf @@ -30,7 +31,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.BottomStart -import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.TopEnd import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -50,7 +50,6 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -75,6 +74,7 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_ENDED import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED +import com.vitorpamplona.quartz.events.Participant import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -372,6 +372,18 @@ private fun RenderNoteRow( } } +@Immutable +data class LiveActivityCard( + val name: String, + val cover: String?, + val media: String?, + val subject: String?, + val content: String?, + val participants: ImmutableList, + val status: String?, + val starts: Long? +) + @Composable fun RenderLiveActivityThumb( baseNote: Note, @@ -380,38 +392,279 @@ fun RenderLiveActivityThumb( ) { val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return - val eventUpdates by baseNote.live().metadata.observeAsState() + val card by baseNote.live().metadata.map { + val noteEvent = it.note.event as? LiveActivitiesEvent - val media = remember(eventUpdates) { noteEvent.streaming() } - val cover by remember(eventUpdates) { - derivedStateOf { - noteEvent.image()?.ifBlank { null } - } - } - val subject = remember(eventUpdates) { noteEvent.title()?.ifBlank { null } } - val content = remember(eventUpdates) { noteEvent.summary() } - val participants = remember(eventUpdates) { noteEvent.participants() } - val status = remember(eventUpdates) { noteEvent.status() } - val starts = remember(eventUpdates) { noteEvent.starts() } + LiveActivityCard( + name = noteEvent?.dTag() ?: "", + cover = noteEvent?.image()?.ifBlank { null }, + media = noteEvent?.streaming(), + subject = noteEvent?.title()?.ifBlank { null }, + content = noteEvent?.summary(), + participants = noteEvent?.participants()?.toImmutableList() ?: persistentListOf(), + status = noteEvent?.status(), + starts = noteEvent?.starts() + ) + }.distinctUntilChanged().observeAsState( + LiveActivityCard( + name = noteEvent.dTag(), + cover = noteEvent.image()?.ifBlank { null }, + media = noteEvent.streaming(), + subject = noteEvent.title()?.ifBlank { null }, + content = noteEvent.summary(), + participants = noteEvent.participants().toImmutableList(), + status = noteEvent.status(), + starts = noteEvent.starts() + ) + ) var isOnline by remember { mutableStateOf(false) } - LaunchedEffect(key1 = media) { - launch(Dispatchers.IO) { - val newIsOnline = OnlineChecker.isOnline(media) + LaunchedEffect(key1 = card.media) { + accountViewModel.checkIsOnline(card.media) { newIsOnline -> if (isOnline != newIsOnline) { isOnline = newIsOnline } } } + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + contentAlignment = TopEnd, + modifier = Modifier + .aspectRatio(ratio = 16f / 9f) + .fillMaxWidth() + ) { + card.cover?.let { + AsyncImage( + model = it, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .clip(QuoteBorder) + ) + } ?: run { + baseNote.author?.let { + DisplayAuthorBanner(it) + } + } + + Box(Modifier.padding(10.dp)) { + Crossfade(targetState = card.status) { + when (it) { + STATUS_LIVE -> { + if (card.media.isNullOrBlank()) { + LiveFlag() + } else if (isOnline) { + LiveFlag() + } else { + OfflineFlag() + } + } + STATUS_ENDED -> { + EndedFlag() + } + STATUS_PLANNED -> { + ScheduledFlag(card.starts) + } + else -> { + EndedFlag() + } + } + } + } + + LoadParticipants(card.participants, baseNote, accountViewModel) { participantUsers -> + Box( + Modifier + .padding(10.dp) + .align(BottomStart) + ) { + if (participantUsers.isNotEmpty()) { + Gallery(participantUsers, accountViewModel) + } + } + } + } + + Spacer(modifier = DoubleVertSpacer) + + ChannelHeader( + channelHex = remember { baseNote.idHex }, + showVideo = false, + showBottomDiviser = false, + showFlag = false, + sendToChannel = true, + modifier = remember { + Modifier.padding(start = 0.dp, end = 0.dp, top = 5.dp, bottom = 5.dp) + }, + accountViewModel = accountViewModel, + nav = nav + ) + } +} + +@Immutable +data class CommunityCard( + val name: String, + val description: String?, + val cover: String?, + val moderators: ImmutableList +) + +@Composable +fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { + val noteEvent = baseNote.event as? CommunityDefinitionEvent ?: return + + val card by baseNote.live().metadata.map { + val noteEvent = it.note.event as? CommunityDefinitionEvent + + CommunityCard( + name = noteEvent?.dTag() ?: "", + description = noteEvent?.description(), + cover = noteEvent?.image()?.ifBlank { null }, + moderators = noteEvent?.moderators()?.toImmutableList() ?: persistentListOf() + ) + }.distinctUntilChanged().observeAsState( + CommunityCard( + name = noteEvent.dTag(), + description = noteEvent.description(), + cover = noteEvent.image()?.ifBlank { null }, + moderators = noteEvent.moderators().toImmutableList() + ) + ) + + Row(Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth(0.3f) + .aspectRatio(ratio = 1f) + ) { + card.cover?.let { + Box(contentAlignment = BottomStart) { + AsyncImage( + model = it, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .clip(QuoteBorder) + ) + } + } ?: run { + baseNote.author?.let { + DisplayAuthorBanner(it) + } + } + } + + Spacer(modifier = DoubleHorzSpacer) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = card.name, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + + Spacer(modifier = StdHorzSpacer) + LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav) + Spacer(modifier = StdHorzSpacer) + ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav) + } + + card.description?.let { + Spacer(modifier = StdVertSpacer) + Row() { + Text( + text = it, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + fontSize = 14.sp + ) + } + } + + LoadModerators(card.moderators, baseNote, accountViewModel) { participantUsers -> + if (participantUsers.isNotEmpty()) { + Spacer(modifier = StdVertSpacer) + Row(modifier = Modifier.fillMaxWidth()) { + Gallery(participantUsers, accountViewModel) + } + } + } + } + } +} + +@Composable +fun LoadModerators( + moderators: ImmutableList, + baseNote: Note, + accountViewModel: AccountViewModel, + content: @Composable (ImmutableList) -> Unit +) { var participantUsers by remember { mutableStateOf>( persistentListOf() ) } - LaunchedEffect(key1 = eventUpdates) { + LaunchedEffect(key1 = moderators) { + launch(Dispatchers.IO) { + val hosts = moderators.mapNotNull { part -> + if (part.key != baseNote.author?.pubkeyHex) { + LocalCache.checkGetOrCreateUser(part.key) + } else { + null + } + } + + val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList) + val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts) + + val newParticipantUsers = if (followingKeySet == null) { + val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS) + val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts) + + (hosts + followingParticipants + (allParticipants - followingParticipants)).toImmutableList() + } else { + (hosts + allParticipants).toImmutableList() + } + + if (!equalImmutableLists(newParticipantUsers, participantUsers)) { + participantUsers = newParticipantUsers + } + } + } + + content(participantUsers) +} + +@Composable +private fun LoadParticipants( + participants: ImmutableList, + baseNote: Note, + accountViewModel: AccountViewModel, + inner: @Composable (ImmutableList) -> Unit +) { + var participantUsers by remember { + mutableStateOf>( + persistentListOf() + ) + } + + LaunchedEffect(key1 = participants) { launch(Dispatchers.IO) { val hosts = participants.mapNotNull { part -> if (part.key != baseNote.author?.pubkeyHex) { @@ -445,198 +698,7 @@ fun RenderLiveActivityThumb( } } - Column( - modifier = Modifier.fillMaxWidth() - ) { - Box( - contentAlignment = TopEnd, - modifier = Modifier - .aspectRatio(ratio = 16f / 9f) - .fillMaxWidth() - ) { - cover?.let { - AsyncImage( - model = it, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .clip(QuoteBorder) - ) - } ?: run { - baseNote.author?.let { - DisplayAuthorBanner(it) - } - } - - Box(Modifier.padding(10.dp)) { - Crossfade(targetState = status) { - when (it) { - STATUS_LIVE -> { - if (media.isNullOrBlank()) { - LiveFlag() - } else if (isOnline) { - LiveFlag() - } else { - OfflineFlag() - } - } - STATUS_ENDED -> { - EndedFlag() - } - STATUS_PLANNED -> { - ScheduledFlag(starts) - } - else -> { - EndedFlag() - } - } - } - } - - Box( - Modifier - .padding(10.dp) - .align(BottomStart) - ) { - if (participantUsers.isNotEmpty()) { - Gallery(participantUsers, accountViewModel) - } - } - } - - Spacer(modifier = DoubleVertSpacer) - - ChannelHeader( - channelHex = remember { baseNote.idHex }, - showVideo = false, - showBottomDiviser = false, - showFlag = false, - sendToChannel = true, - modifier = remember { - Modifier.padding(start = 0.dp, end = 0.dp, top = 5.dp, bottom = 5.dp) - }, - accountViewModel = accountViewModel, - nav = nav - ) - } -} - -@Composable -fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - val noteEvent = baseNote.event as? CommunityDefinitionEvent ?: return - - val eventUpdates by baseNote.live().metadata.observeAsState() - - val name = remember(eventUpdates) { noteEvent.dTag() } - val description = remember(eventUpdates) { noteEvent.description() } - val cover by remember(eventUpdates) { - derivedStateOf { - noteEvent.image()?.ifBlank { null } - } - } - val moderators = remember(eventUpdates) { noteEvent.moderators() } - - var participantUsers by remember { - mutableStateOf>( - persistentListOf() - ) - } - - LaunchedEffect(key1 = eventUpdates) { - launch(Dispatchers.IO) { - val hosts = moderators.mapNotNull { part -> - if (part.key != baseNote.author?.pubkeyHex) { - LocalCache.checkGetOrCreateUser(part.key) - } else { - null - } - } - - val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList) - val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts) - - val newParticipantUsers = if (followingKeySet == null) { - val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS) - val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts) - - (hosts + followingParticipants + (allParticipants - followingParticipants)).toImmutableList() - } else { - (hosts + allParticipants).toImmutableList() - } - - if (!equalImmutableLists(newParticipantUsers, participantUsers)) { - participantUsers = newParticipantUsers - } - } - } - - Row(Modifier.fillMaxWidth()) { - Column( - modifier = Modifier - .fillMaxWidth(0.3f) - .aspectRatio(ratio = 1f) - ) { - cover?.let { - Box(contentAlignment = BottomStart) { - AsyncImage( - model = it, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .clip(QuoteBorder) - ) - } - } ?: run { - baseNote.author?.let { - DisplayAuthorBanner(it) - } - } - } - - Spacer(modifier = DoubleHorzSpacer) - - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.SpaceBetween - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = name, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f) - ) - - Spacer(modifier = StdHorzSpacer) - LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav) - Spacer(modifier = StdHorzSpacer) - ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colorScheme.onSurface, accountViewModel = accountViewModel, nav = nav) - } - - description?.let { - Spacer(modifier = StdVertSpacer) - Row() { - Text( - text = it, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - fontSize = 14.sp - ) - } - } - - if (participantUsers.isNotEmpty()) { - Spacer(modifier = StdVertSpacer) - Row(modifier = Modifier.fillMaxWidth()) { - Gallery(participantUsers, accountViewModel) - } - } - } - } + inner(participantUsers) } @Composable