diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 152428522..8a2781771 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -852,8 +852,6 @@ object LocalCache { note.loadEvent(event, author, replyTo) - Log.d("CM", "AAA REPLY TO ${event.content} ${event.taggedEvents()}") - // Counts the replies replyTo.forEach { it.addReply(note) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 7d701f13e..c3a626bde 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -375,7 +375,14 @@ open class Note(val idHex: String) { } fun isNewThread(): Boolean { - return event is RepostEvent || event is GenericRepostEvent || replyTo == null || replyTo?.size == 0 + return ( + event is RepostEvent || + event is GenericRepostEvent || + replyTo == null || + replyTo?.size == 0 + ) && + event !is ChannelMessageEvent && + event !is LiveActivitiesChatMessageEvent } fun hasZapped(loggedIn: User): Boolean { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index c87cf68ba..4b2cc8356 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -5,6 +5,8 @@ import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.GenericRepostEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent +import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent +import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.PinListEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent @@ -68,7 +70,9 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { PollNoteEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, - PinListEvent.kind + PinListEvent.kind, + LiveActivitiesChatMessageEvent.kind, + LiveActivitiesEvent.kind ), authors = followSet, limit = 400, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index 390969169..4ee3b1b26 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -3,6 +3,8 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent @@ -27,7 +29,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter() { + override fun feedKey(): String { + return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList + } + + override fun feed(): List { + val longFormNotes = innerApplyFilter(LocalCache.addressables.values) + + return sort(longFormNotes) + } + + override fun applyFilter(collection: Set): Set { + return innerApplyFilter(collection) + } + + private fun innerApplyFilter(collection: Collection): Set { + checkNotInMainThread() + + val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() + val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() + + val fortyEightHrs = (Date().time / 1000) - 60 * 60 * 48 // hrs + + return collection + .asSequence() + .filter { it -> + val noteEvent = it.event + (noteEvent is LiveActivitiesEvent && noteEvent.createdAt > fortyEightHrs && noteEvent.status() == "live" && checkIfOnline(noteEvent.streaming())) && + (it.author?.pubkeyHex in followingKeySet || (noteEvent.isTaggedHashes(followingTagSet))) && + // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable + it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true + } + .toSet() + } + + override fun sort(collection: Set): List { + return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + } +} + +fun checkIfOnline(url: String?): Boolean { + if (url.isNullOrBlank()) return false + + val request = Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .url(url) + .get() + .build() + + return try { + HttpClient.getHttpClient().newCall(request).execute().code == 200 + } catch (e: Exception) { + Log.e("LiveActivities", "Failed to check streaming url $url", e) + false + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index d72aa2b06..1d8053a3a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -13,6 +13,7 @@ import com.vitorpamplona.amethyst.ui.note.UserReactionsViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedLiveActivitiesViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel @@ -38,6 +39,7 @@ import kotlinx.coroutines.launch fun AppNavigation( homeFeedViewModel: NostrHomeFeedViewModel, repliesFeedViewModel: NostrHomeRepliesFeedViewModel, + liveActivitiesViewModel: NostrHomeFeedLiveActivitiesViewModel, knownFeedViewModel: NostrChatroomListKnownFeedViewModel, newFeedViewModel: NostrChatroomListNewFeedViewModel, searchFeedViewModel: NostrGlobalFeedViewModel, @@ -67,6 +69,7 @@ fun AppNavigation( HomeScreen( homeFeedViewModel = homeFeedViewModel, repliesFeedViewModel = repliesFeedViewModel, + liveActivitiesViewModel = liveActivitiesViewModel, accountViewModel = accountViewModel, nav = nav, nip47 = nip47 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index e6e076279..eca41fb34 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -255,7 +255,6 @@ fun ChatroomMessageCompose( } val replyTo = note.replyTo - println("AAA replyTo ${replyTo?.lastOrNull()}") if (!innerQuote && !replyTo.isNullOrEmpty()) { Row(verticalAlignment = Alignment.CenterVertically) { replyTo.lastOrNull()?.let { note -> 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 d63e368c9..2e008f569 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 @@ -92,6 +92,7 @@ import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent +import com.vitorpamplona.amethyst.service.model.BaseTextNoteEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent @@ -379,7 +380,7 @@ fun NormalNote( val channelHex = remember { baseNote.channelHex() } if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && channelHex != null) { - ChannelHeader(channelHex = channelHex, accountViewModel = accountViewModel, nav = nav) + ChannelHeader(channelHex = channelHex, showVideo = !makeItShort, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is BadgeDefinitionEvent) { BadgeDisplay(baseNote = baseNote) } else if (noteEvent is FileHeaderEvent) { @@ -1648,7 +1649,8 @@ private fun ReplyRow( val showChannelReply by remember { derivedStateOf { - noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser()) + (noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) || + (noteEvent is LiveActivitiesChatMessageEvent && (note.replyTo != null || noteEvent.hasAnyTaggedUser())) } } @@ -1663,10 +1665,18 @@ private fun ReplyRow( } else if (showChannelReply) { val channelHex = note.channelHex() channelHex?.let { - val replies = remember { note.replyTo?.toImmutableList() } - val mentions = remember { (note.event as? ChannelMessageEvent)?.mentions()?.toImmutableList() ?: persistentListOf() } + ChannelHeader( + channelHex = channelHex, + showVideo = false, + modifier = remember { Modifier.padding(vertical = 10.dp) }, + accountViewModel = accountViewModel, + nav = nav + ) - ReplyInformationChannel(replies, mentions, it, accountViewModel, nav) + val replies = remember { note.replyTo?.toImmutableList() } + val mentions = remember { (note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf() } + + ReplyInformationChannel(replies, mentions, accountViewModel, nav) Spacer(modifier = Modifier.height(5.dp)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt index 4998f0623..f71c527f6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt @@ -128,7 +128,6 @@ private fun ReplyInformation( fun ReplyInformationChannel( replyTo: ImmutableList?, mentions: ImmutableList, - channelHex: String, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { @@ -145,67 +144,25 @@ fun ReplyInformationChannel( } if (sortedMentions != null) { - LoadChannel(channelHex) { channel -> - ReplyInformationChannel( - replyTo, - sortedMentions, - channel, - onUserTagClick = { - nav("User/${it.pubkeyHex}") - }, - onChannelTagClick = { - nav("Channel/${it.idHex}") - } - ) - } + ReplyInformationChannel( + replyTo, + sortedMentions, + onUserTagClick = { + nav("User/${it.pubkeyHex}") + } + ) } } -@Composable -fun ReplyInformationChannel(replyTo: ImmutableList?, mentions: ImmutableList?, channel: Channel, nav: (String) -> Unit) { - ReplyInformationChannel( - replyTo, - mentions, - channel, - onUserTagClick = { - nav("User/${it.pubkeyHex}") - }, - onChannelTagClick = { - nav("Channel/${it.idHex}") - } - ) -} - @OptIn(ExperimentalLayoutApi::class) @Composable fun ReplyInformationChannel( replyTo: ImmutableList?, mentions: ImmutableList?, - baseChannel: Channel, prefix: String = "", - onUserTagClick: (User) -> Unit, - onChannelTagClick: (Channel) -> Unit + onUserTagClick: (User) -> Unit ) { - val channelState by baseChannel.live.observeAsState() - val channel = channelState?.channel ?: return - - val channelName = remember(channelState) { - AnnotatedString("${channel.toBestDisplayName()} ") - } - FlowRow() { - Text( - stringResource(R.string.in_channel), - fontSize = 13.sp, - color = MaterialTheme.colors.placeholderText - ) - - ClickableText( - text = channelName, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.lessImportantLink, fontSize = 13.sp), - onClick = { onChannelTagClick(channel) } - ) - if (mentions != null && mentions.isNotEmpty()) { if (replyTo != null && replyTo.isNotEmpty()) { Text( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 5d502ae63..0df60229a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter +import com.vitorpamplona.amethyst.ui.dal.HomeLiveActivitiesFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter @@ -99,6 +100,15 @@ class NostrChatroomListNewFeedViewModel(val account: Account) : FeedViewModel(Ch } } +@Stable +class NostrHomeFeedLiveActivitiesViewModel(val account: Account) : FeedViewModel(HomeLiveActivitiesFeedFilter(account)) { + class Factory(val account: Account) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrHomeFeedLiveActivitiesViewModel { + return NostrHomeFeedLiveActivitiesViewModel(account) as NostrHomeFeedLiveActivitiesViewModel + } + } +} + @Stable class NostrHomeFeedViewModel(val account: Account) : FeedViewModel(HomeNewThreadFeedFilter(account)) { class Factory(val account: Account) : ViewModelProvider.Factory { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 258c1af99..f93bf3ef5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -378,7 +378,7 @@ fun NoteMaster( ) { Column() { if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && note.channelHex() != null) { - ChannelHeader(channelHex = note.channelHex()!!, accountViewModel = accountViewModel, nav = nav) + ChannelHeader(channelHex = note.channelHex()!!, showVideo = true, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is FileHeaderEvent) { FileHeaderDisplay(baseNote) } else if (noteEvent is FileStorageHeaderEvent) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 9b58aca75..5245548a3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -53,6 +53,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor @@ -98,6 +99,9 @@ import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.SmallBorder +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -203,8 +207,9 @@ fun ChannelScreen( Column(Modifier.fillMaxHeight()) { ChannelHeader( - channel, - accountViewModel, + baseChannel = channel, + showVideo = true, + accountViewModel = accountViewModel, nav = nav ) @@ -472,8 +477,14 @@ fun MyTextField( } @Composable -fun ChannelHeader(channelHex: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - var baseChannel by remember { mutableStateOf(null) } +fun ChannelHeader( + channelHex: String, + showVideo: Boolean, + modifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + var baseChannel by remember { mutableStateOf(LocalCache.channels[channelHex]) } val scope = rememberCoroutineScope() LaunchedEffect(key1 = channelHex) { @@ -483,12 +494,24 @@ fun ChannelHeader(channelHex: String, accountViewModel: AccountViewModel, nav: ( } baseChannel?.let { - ChannelHeader(it, accountViewModel, nav) + ChannelHeader( + it, + showVideo, + modifier, + accountViewModel, + nav + ) } } @Composable -fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +fun ChannelHeader( + baseChannel: Channel, + showVideo: Boolean, + modifier: Modifier = StdPadding, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { val channelState by baseChannel.live.observeAsState() val channel = remember(channelState) { channelState?.channel } ?: return @@ -506,7 +529,7 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, nav: } } - if (streamingUrl != null) { + if (streamingUrl != null && showVideo) { Row(verticalAlignment = Alignment.CenterVertically) { VideoView( videoUri = streamingUrl!!, @@ -516,7 +539,7 @@ fun ChannelHeader(baseChannel: Channel, accountViewModel: AccountViewModel, nav: } } - Column(modifier = Modifier.padding(12.dp)) { + Column(modifier = modifier) { Row(verticalAlignment = Alignment.CenterVertically) { RobohashAsyncImageProxy( robot = channel.idHex, @@ -624,12 +647,31 @@ private fun LiveChannelActionOptions( } } + val status by remember { + derivedStateOf { + channel.info?.status() + } + } + if (isMe) { // EditButton(accountViewModel, channel) } else { LocalCache.addressables[channel.idHex]?.let { + if (status == "live") { + Text( + text = "LIVE", + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = Modifier + .clip(SmallBorder) + .drawBehind { drawRect(Color.Red) } + .padding(horizontal = 5.dp) + ) + Spacer(modifier = StdHorzSpacer) + } + LikeReaction(it, MaterialTheme.colors.onSurface, accountViewModel) - Spacer(modifier = Modifier.width(5.dp)) + Spacer(modifier = StdHorzSpacer) ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 316768847..476900af4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -1,9 +1,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.material.MaterialTheme @@ -14,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -30,7 +37,9 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog +import com.vitorpamplona.amethyst.ui.screen.FeedState import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedLiveActivitiesViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys @@ -47,6 +56,7 @@ import kotlinx.coroutines.launch fun HomeScreen( homeFeedViewModel: NostrHomeFeedViewModel, repliesFeedViewModel: NostrHomeRepliesFeedViewModel, + liveActivitiesViewModel: NostrHomeFeedLiveActivitiesViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit, nip47: String? = null @@ -88,7 +98,7 @@ fun HomeScreen( Column( modifier = Modifier.padding(vertical = 0.dp) ) { - HomePages(pagerState, tabs, accountViewModel, nav) + HomePages(pagerState, tabs, liveActivitiesViewModel, accountViewModel, nav) } } } @@ -98,6 +108,7 @@ fun HomeScreen( private fun HomePages( pagerState: PagerState, tabs: ImmutableList, + liveActivitiesViewModel: NostrHomeFeedLiveActivitiesViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { @@ -120,6 +131,12 @@ private fun HomePages( } } + LiveActivities( + liveActivitiesViewModel = liveActivitiesViewModel, + accountViewModel = accountViewModel, + nav = nav + ) + HorizontalPager(pageCount = 2, state = pagerState) { page -> RefresheableFeedView( viewModel = tabs[page].viewModel, @@ -131,6 +148,60 @@ private fun HomePages( } } +@Composable +fun LiveActivities( + liveActivitiesViewModel: NostrHomeFeedLiveActivitiesViewModel, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + val feedState by liveActivitiesViewModel.feedContent.collectAsState() + + Crossfade( + targetState = feedState, + animationSpec = tween(durationMillis = 100) + ) { state -> + when (state) { + is FeedState.Loaded -> { + FeedLoaded( + state, + accountViewModel, + nav + ) + } + + else -> { + } + } + } +} + +@Composable +private fun FeedLoaded( + state: FeedState.Loaded, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + val listState = rememberLazyListState() + + LazyColumn( + contentPadding = PaddingValues( + top = 10.dp, + bottom = 10.dp + ), + state = listState + ) { + itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> + ChannelHeader( + channelHex = item.idHex, + showVideo = false, + modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + accountViewModel = accountViewModel, + nav = nav + ) + } + } +} + @Composable fun WatchAccountForHomeScreen( homeFeedViewModel: NostrHomeFeedViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 1b9747c3f..9fb6e3387 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedLiveActivitiesViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel @@ -89,6 +90,11 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun factory = NostrHomeRepliesFeedViewModel.Factory(accountViewModel.account) ) + val liveActivitiesViewModel: NostrHomeFeedLiveActivitiesViewModel = viewModel( + key = accountViewModel.userProfile().pubkeyHex + "NostrHomeLiveActivitiesFeedViewModel", + factory = NostrHomeFeedLiveActivitiesViewModel.Factory(accountViewModel.account) + ) + val searchFeedViewModel: NostrGlobalFeedViewModel = viewModel( key = accountViewModel.userProfile().pubkeyHex + "NostrGlobalFeedViewModel", factory = NostrGlobalFeedViewModel.Factory(accountViewModel.account) @@ -184,6 +190,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun AppNavigation( homeFeedViewModel, repliesFeedViewModel, + liveActivitiesViewModel, knownFeedViewModel, newFeedViewModel, searchFeedViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 2d27981f9..99ad63f23 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.theme +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape @@ -25,3 +26,5 @@ val StdHorzSpacer = Modifier.width(5.dp) val DoubleHorzSpacer = Modifier.width(10.dp) val Size35dp = 35.dp + +val StdPadding = Modifier.padding(10.dp)