Breaks down Compose components in the Discovery tab.

This commit is contained in:
Vitor Pamplona 2023-10-02 17:43:38 -04:00
parent 188ef3762d
commit cec204b7ae

View File

@ -19,6 +19,7 @@ import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -30,7 +31,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.BottomStart 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.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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_ENDED
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED
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 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<Participant>,
val status: String?,
val starts: Long?
)
@Composable @Composable
fun RenderLiveActivityThumb( fun RenderLiveActivityThumb(
baseNote: Note, baseNote: Note,
@ -380,38 +392,279 @@ fun RenderLiveActivityThumb(
) { ) {
val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return 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() } LiveActivityCard(
val cover by remember(eventUpdates) { name = noteEvent?.dTag() ?: "",
derivedStateOf { cover = noteEvent?.image()?.ifBlank { null },
noteEvent.image()?.ifBlank { null } media = noteEvent?.streaming(),
} subject = noteEvent?.title()?.ifBlank { null },
} content = noteEvent?.summary(),
val subject = remember(eventUpdates) { noteEvent.title()?.ifBlank { null } } participants = noteEvent?.participants()?.toImmutableList() ?: persistentListOf(),
val content = remember(eventUpdates) { noteEvent.summary() } status = noteEvent?.status(),
val participants = remember(eventUpdates) { noteEvent.participants() } starts = noteEvent?.starts()
val status = remember(eventUpdates) { noteEvent.status() } )
val starts = remember(eventUpdates) { 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) } var isOnline by remember { mutableStateOf(false) }
LaunchedEffect(key1 = media) { LaunchedEffect(key1 = card.media) {
launch(Dispatchers.IO) { accountViewModel.checkIsOnline(card.media) { newIsOnline ->
val newIsOnline = OnlineChecker.isOnline(media)
if (isOnline != newIsOnline) { if (isOnline != newIsOnline) {
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<Participant>
)
@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<Participant>,
baseNote: Note,
accountViewModel: AccountViewModel,
content: @Composable (ImmutableList<User>) -> Unit
) {
var participantUsers by remember { var participantUsers by remember {
mutableStateOf<ImmutableList<User>>( mutableStateOf<ImmutableList<User>>(
persistentListOf() 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<Participant>,
baseNote: Note,
accountViewModel: AccountViewModel,
inner: @Composable (ImmutableList<User>) -> Unit
) {
var participantUsers by remember {
mutableStateOf<ImmutableList<User>>(
persistentListOf()
)
}
LaunchedEffect(key1 = participants) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val hosts = participants.mapNotNull { part -> val hosts = participants.mapNotNull { part ->
if (part.key != baseNote.author?.pubkeyHex) { if (part.key != baseNote.author?.pubkeyHex) {
@ -445,198 +698,7 @@ fun RenderLiveActivityThumb(
} }
} }
Column( inner(participantUsers)
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<ImmutableList<User>>(
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)
}
}
}
}
} }
@Composable @Composable