Creates a feed for follow packs
This commit is contained in:
@@ -1691,6 +1691,10 @@ class Account(
|
|||||||
|
|
||||||
fun isFollowing(user: HexKey): Boolean = user in followingKeySet()
|
fun isFollowing(user: HexKey): Boolean = user in followingKeySet()
|
||||||
|
|
||||||
|
fun isKnown(user: User): Boolean = user.pubkeyHex in allFollows.flow.value.authors
|
||||||
|
|
||||||
|
fun isKnown(user: HexKey): Boolean = user in allFollows.flow.value.authors
|
||||||
|
|
||||||
fun isAcceptable(note: Note): Boolean {
|
fun isAcceptable(note: Note): Boolean {
|
||||||
return note.author?.let { isAcceptable(it) } ?: true &&
|
return note.author?.let { isAcceptable(it) } ?: true &&
|
||||||
// if user hasn't hided this author
|
// if user hasn't hided this author
|
||||||
|
|||||||
+2
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dataso
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssembler
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource.FollowPackFeedFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
||||||
@@ -77,6 +78,7 @@ class RelaySubscriptionsCoordinator(
|
|||||||
val profile = UserProfileFilterAssembler(client)
|
val profile = UserProfileFilterAssembler(client)
|
||||||
val hashtags = HashtagFilterAssembler(client)
|
val hashtags = HashtagFilterAssembler(client)
|
||||||
val geohashes = GeoHashFilterAssembler(client)
|
val geohashes = GeoHashFilterAssembler(client)
|
||||||
|
val followPacks = FollowPackFeedFilterAssembler(client)
|
||||||
|
|
||||||
// active when sending zaps via NWC
|
// active when sending zaps via NWC
|
||||||
val nwc = NWCPaymentFilterAssembler(client)
|
val nwc = NWCPaymentFilterAssembler(client)
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPackFeedScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
|
||||||
@@ -149,6 +150,7 @@ fun AppNavigation(
|
|||||||
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
|
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||||
|
composableFromEndArgs<Route.FollowPack> { FollowPackFeedScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, it.expiresDays, accountViewModel, nav) }
|
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, it.expiresDays, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
|
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
|
||||||
|
|||||||
+71
-59
@@ -43,6 +43,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
|||||||
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||||
@@ -65,73 +66,82 @@ fun routeFor(
|
|||||||
fun routeFor(
|
fun routeFor(
|
||||||
noteEvent: Event,
|
noteEvent: Event,
|
||||||
loggedIn: Account,
|
loggedIn: Account,
|
||||||
): Route? {
|
): Route? =
|
||||||
if (noteEvent is DraftWrapEvent) {
|
if (noteEvent is DraftWrapEvent) {
|
||||||
val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent)
|
val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent)
|
||||||
|
|
||||||
if (innerEvent is IsInPublicChatChannel) {
|
if (innerEvent != null) {
|
||||||
innerEvent.channelId()?.let {
|
routeForInner(innerEvent, loggedIn)
|
||||||
return Route.PublicChatChannel(it)
|
|
||||||
}
|
|
||||||
} else if (innerEvent is LiveActivitiesEvent) {
|
|
||||||
innerEvent.address().let {
|
|
||||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
|
||||||
}
|
|
||||||
} else if (innerEvent is LiveActivitiesChatMessageEvent) {
|
|
||||||
innerEvent.activityAddress()?.let {
|
|
||||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
|
||||||
}
|
|
||||||
} else if (innerEvent is EphemeralChatEvent) {
|
|
||||||
innerEvent.roomId()?.let {
|
|
||||||
return Route.EphemeralChat(it.id, it.relayUrl.url)
|
|
||||||
}
|
|
||||||
} else if (innerEvent is ChatroomKeyable) {
|
|
||||||
val room = innerEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
|
|
||||||
loggedIn.chatroomList.getOrCreatePrivateChatroom(room)
|
|
||||||
return Route.Room(room)
|
|
||||||
} else if (innerEvent is AddressableEvent) {
|
|
||||||
return Route.Note(noteEvent.aTag().toTag())
|
|
||||||
} else {
|
} else {
|
||||||
return Route.Note(noteEvent.id)
|
Route.Note(noteEvent.id)
|
||||||
}
|
}
|
||||||
} else if (noteEvent is AppDefinitionEvent) {
|
|
||||||
return Route.ContentDiscovery(noteEvent.id)
|
|
||||||
} else if (noteEvent is IsInPublicChatChannel) {
|
|
||||||
noteEvent.channelId()?.let {
|
|
||||||
return Route.PublicChatChannel(it)
|
|
||||||
}
|
|
||||||
} else if (noteEvent is ChannelCreateEvent) {
|
|
||||||
return Route.PublicChatChannel(noteEvent.id)
|
|
||||||
} else if (noteEvent is LiveActivitiesEvent) {
|
|
||||||
noteEvent.address().let {
|
|
||||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
|
||||||
}
|
|
||||||
} else if (noteEvent is LiveActivitiesChatMessageEvent) {
|
|
||||||
noteEvent.activityAddress()?.let {
|
|
||||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
|
||||||
}
|
|
||||||
} else if (noteEvent is ChatroomKeyable) {
|
|
||||||
val room = noteEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
|
|
||||||
loggedIn.chatroomList.getOrCreatePrivateChatroom(room)
|
|
||||||
return Route.Room(room)
|
|
||||||
} else if (noteEvent is CommunityDefinitionEvent) {
|
|
||||||
return Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag())
|
|
||||||
} else if (noteEvent is GiftWrapEvent) {
|
|
||||||
noteEvent.innerEventId?.let {
|
|
||||||
return routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
|
||||||
}
|
|
||||||
} else if (noteEvent is SealedRumorEvent) {
|
|
||||||
noteEvent.innerEventId?.let {
|
|
||||||
return routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
|
||||||
}
|
|
||||||
} else if (noteEvent is AddressableEvent) {
|
|
||||||
return Route.Note(noteEvent.aTag().toTag())
|
|
||||||
} else {
|
} else {
|
||||||
return Route.Note(noteEvent.id)
|
routeForInner(noteEvent, loggedIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
fun routeForInner(
|
||||||
}
|
noteEvent: Event,
|
||||||
|
loggedIn: Account,
|
||||||
|
): Route? =
|
||||||
|
when (noteEvent) {
|
||||||
|
is AppDefinitionEvent -> {
|
||||||
|
Route.ContentDiscovery(noteEvent.id)
|
||||||
|
}
|
||||||
|
is IsInPublicChatChannel -> {
|
||||||
|
noteEvent.channelId()?.let {
|
||||||
|
Route.PublicChatChannel(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ChannelCreateEvent -> {
|
||||||
|
Route.PublicChatChannel(noteEvent.id)
|
||||||
|
}
|
||||||
|
is LiveActivitiesEvent -> {
|
||||||
|
noteEvent.address().let {
|
||||||
|
Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LiveActivitiesChatMessageEvent -> {
|
||||||
|
noteEvent.activityAddress()?.let {
|
||||||
|
Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is EphemeralChatEvent -> {
|
||||||
|
noteEvent.roomId()?.let {
|
||||||
|
Route.EphemeralChat(it.id, it.relayUrl.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is FollowListEvent -> {
|
||||||
|
Route.FollowPack(noteEvent.address())
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChatroomKeyable -> {
|
||||||
|
val room = noteEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
|
||||||
|
loggedIn.chatroomList.getOrCreatePrivateChatroom(room)
|
||||||
|
Route.Room(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
is CommunityDefinitionEvent -> {
|
||||||
|
Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag())
|
||||||
|
}
|
||||||
|
is GiftWrapEvent -> {
|
||||||
|
noteEvent.innerEventId?.let {
|
||||||
|
routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is SealedRumorEvent -> {
|
||||||
|
noteEvent.innerEventId?.let {
|
||||||
|
routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is AddressableEvent -> {
|
||||||
|
Route.Note(noteEvent.aTag().toTag())
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Route.Note(noteEvent.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun routeToMessage(
|
fun routeToMessage(
|
||||||
user: HexKey,
|
user: HexKey,
|
||||||
@@ -207,6 +217,8 @@ fun routeFor(roomId: RoomId): Route = Route.EphemeralChat(roomId.id, roomId.rela
|
|||||||
|
|
||||||
fun routeFor(user: User): Route.Profile = Route.Profile(user.pubkeyHex)
|
fun routeFor(user: User): Route.Profile = Route.Profile(user.pubkeyHex)
|
||||||
|
|
||||||
|
fun routeForUser(userHex: HexKey): Route.Profile = Route.Profile(userHex)
|
||||||
|
|
||||||
fun authorRouteFor(note: Note): Route.Profile? = note.author?.pubkeyHex?.let { Route.Profile(it) }
|
fun authorRouteFor(note: Note): Route.Profile? = note.author?.pubkeyHex?.let { Route.Profile(it) }
|
||||||
|
|
||||||
fun routeReplyTo(
|
fun routeReplyTo(
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.navigation.routes
|
|||||||
import androidx.navigation.NavDestination.Companion.hasRoute
|
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route.Room
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -96,7 +98,25 @@ sealed class Route {
|
|||||||
val kind: Int,
|
val kind: Int,
|
||||||
val pubKeyHex: HexKey,
|
val pubKeyHex: HexKey,
|
||||||
val dTag: String,
|
val dTag: String,
|
||||||
) : Route()
|
) : Route() {
|
||||||
|
constructor(address: Address) : this(
|
||||||
|
kind = address.kind,
|
||||||
|
pubKeyHex = address.pubKeyHex,
|
||||||
|
dTag = address.dTag,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable data class FollowPack(
|
||||||
|
val kind: Int,
|
||||||
|
val pubKeyHex: HexKey,
|
||||||
|
val dTag: String,
|
||||||
|
) : Route() {
|
||||||
|
constructor(address: Address) : this(
|
||||||
|
kind = address.kind,
|
||||||
|
pubKeyHex = address.pubKeyHex,
|
||||||
|
dTag = address.dTag,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable data class PublicChatChannel(
|
@Serializable data class PublicChatChannel(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
|||||||
@@ -708,7 +708,7 @@ private fun RenderNoteRow(
|
|||||||
is BadgeAwardEvent -> RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
|
is BadgeAwardEvent -> RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
is FhirResourceEvent -> RenderFhirResource(baseNote, accountViewModel, nav)
|
is FhirResourceEvent -> RenderFhirResource(baseNote, accountViewModel, nav)
|
||||||
is PeopleListEvent -> DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
is PeopleListEvent -> DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
is FollowListEvent -> DisplayFollowList(baseNote, backgroundColor, accountViewModel, nav)
|
is FollowListEvent -> DisplayFollowList(baseNote, accountViewModel, nav)
|
||||||
is RelaySetEvent -> DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
|
is RelaySetEvent -> DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
is ChatMessageRelayListEvent -> DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
is ChatMessageRelayListEvent -> DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
is AdvertisedRelayListEvent -> DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
|
is AdvertisedRelayListEvent -> DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
|
|||||||
@@ -933,7 +933,7 @@ fun ObserveLikeText(
|
|||||||
inner: @Composable (Int) -> Unit,
|
inner: @Composable (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
val reactionCount by observeNoteReactionCount(baseNote, accountViewModel)
|
val reactionCount by observeNoteReactionCount(baseNote, accountViewModel)
|
||||||
|
println("AABBCC $reactionCount ${baseNote.idHex}")
|
||||||
inner(reactionCount)
|
inner(reactionCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,139 +21,163 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note.types
|
package com.vitorpamplona.amethyst.ui.note.types
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
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.CenterVertically
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap
|
||||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.GalleryUnloaded
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
|
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
|
||||||
import com.vitorpamplona.amethyst.ui.note.getGradient
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.FollowSetCard
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier
|
import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.blackTagModifier
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayFollowList(
|
fun DisplayFollowList(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
backgroundColor: MutableState<Color>,
|
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val noteEvent = baseNote.event as? FollowListEvent ?: return
|
val card =
|
||||||
|
observeNoteEventAndMap(baseNote, accountViewModel) { event: FollowListEvent? ->
|
||||||
var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) }
|
if (event == null) {
|
||||||
|
FollowSetCard(
|
||||||
var expanded by remember { mutableStateOf(false) }
|
name = "",
|
||||||
|
media = "",
|
||||||
val toMembersShow =
|
description = "",
|
||||||
if (expanded) {
|
users = persistentListOf(),
|
||||||
members
|
)
|
||||||
} else {
|
} else {
|
||||||
members.take(3)
|
FollowSetCard(
|
||||||
|
name = event.title()?.ifBlank { null } ?: event.dTag(),
|
||||||
|
media = event.image()?.ifBlank { null },
|
||||||
|
description = event.description(),
|
||||||
|
users = accountViewModel.sortUsersSync(event.followIds()).toImmutableList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val image = noteEvent.image()
|
RenderFollowSetThumbEmbed(
|
||||||
|
card.value,
|
||||||
image?.let {
|
baseNote,
|
||||||
MyAsyncImage(
|
accountViewModel,
|
||||||
imageUrl = it,
|
nav,
|
||||||
contentDescription =
|
|
||||||
stringRes(
|
|
||||||
R.string.preview_card_image_for,
|
|
||||||
it,
|
|
||||||
),
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
mainImageModifier = Modifier.fillMaxWidth(),
|
|
||||||
loadedImageModifier = FollowSetImageModifier,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) },
|
|
||||||
onError = { DefaultImageHeader(baseNote, accountViewModel) },
|
|
||||||
)
|
|
||||||
} ?: run {
|
|
||||||
DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = noteEvent.title() ?: noteEvent.dTag(),
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 10.dp),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
@Composable
|
||||||
accountViewModel.loadUsers(noteEvent.taggedUserIds()) {
|
fun RenderFollowSetThumbEmbed(
|
||||||
members = it
|
card: FollowSetCard,
|
||||||
}
|
baseNote: Note,
|
||||||
}
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxWidth().clickable {
|
||||||
|
nav.nav { routeFor(baseNote, accountViewModel.account) }
|
||||||
|
},
|
||||||
|
verticalArrangement = SpacedBy5dp,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.BottomStart,
|
||||||
|
) {
|
||||||
|
card.media?.let {
|
||||||
|
MyAsyncImage(
|
||||||
|
imageUrl = it,
|
||||||
|
contentDescription = stringRes(R.string.preview_card_image_for, it),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
mainImageModifier = Modifier,
|
||||||
|
loadedImageModifier = FollowSetImageModifier,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) },
|
||||||
|
onError = { DefaultImageHeader(baseNote, accountViewModel) },
|
||||||
|
)
|
||||||
|
} ?: run { DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier) }
|
||||||
|
|
||||||
Box {
|
GalleryUnloaded(card.users, StdPadding, accountViewModel, nav)
|
||||||
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
|
||||||
toMembersShow.forEach { user ->
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
UserCompose(
|
|
||||||
user,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = DividerThickness,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (members.size > 3 && !expanded) {
|
Row(
|
||||||
Row(
|
verticalAlignment = CenterVertically,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
horizontalArrangement = Arrangement.Center,
|
) {
|
||||||
modifier =
|
Text(
|
||||||
Modifier
|
text = card.name,
|
||||||
.align(Alignment.BottomCenter)
|
fontWeight = FontWeight.Bold,
|
||||||
.fillMaxWidth()
|
maxLines = 1,
|
||||||
.background(getGradient(backgroundColor)),
|
overflow = TextOverflow.Ellipsis,
|
||||||
) {
|
modifier = Modifier.weight(1f),
|
||||||
ShowMoreButton { expanded = !expanded }
|
)
|
||||||
}
|
Text(
|
||||||
|
text = stringRes(R.string.follow_list_item_label),
|
||||||
|
color = MaterialTheme.colorScheme.background,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = MaterialTheme.colorScheme.blackTagModifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun RenderFollowSetThumbPreview() {
|
||||||
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
|
ThemeComparisonColumn {
|
||||||
|
RenderFollowSetThumbEmbed(
|
||||||
|
card =
|
||||||
|
FollowSetCard(
|
||||||
|
"Orange Pill Perú",
|
||||||
|
"https://i.postimg.cc/GtDgGY5v/5062563795762785335.jpg",
|
||||||
|
"Desc",
|
||||||
|
persistentListOf(
|
||||||
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
baseNote = Note(""),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = EmptyNav(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -71,7 +72,7 @@ fun DisplayPeopleList(
|
|||||||
|
|
||||||
var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) }
|
var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) }
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val toMembersShow =
|
val toMembersShow =
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
|
|||||||
+3
-2
@@ -1076,11 +1076,12 @@ class AccountViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sortUsersSync(hexList: List<HexKey>): List<HexKey> = hexList.sortedByDescending { account.isKnown(it) }
|
||||||
|
|
||||||
fun loadUsersSync(hexList: List<String>): List<User> =
|
fun loadUsersSync(hexList: List<String>): List<User> =
|
||||||
hexList
|
hexList
|
||||||
.mapNotNull { hex -> checkGetOrCreateUser(hex) }
|
.mapNotNull { hex -> checkGetOrCreateUser(hex) }
|
||||||
.sortedBy { account.isFollowing(it) }
|
.sortedByDescending { account.isKnown(it) }
|
||||||
.reversed()
|
|
||||||
|
|
||||||
suspend fun checkVideoIsOnline(videoUrl: String): Boolean =
|
suspend fun checkVideoIsOnline(videoUrl: String): Boolean =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|||||||
+11
-12
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
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
|
||||||
@@ -40,12 +39,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
|
||||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.Gallery
|
import com.vitorpamplona.amethyst.ui.note.GalleryUnloaded
|
||||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
@@ -58,10 +56,11 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier
|
import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
|
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@@ -72,7 +71,7 @@ data class FollowSetCard(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val media: String?,
|
val media: String?,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val users: ImmutableList<User>,
|
val users: ImmutableList<HexKey>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -90,7 +89,7 @@ fun RenderFollowSetThumb(
|
|||||||
description = noteEvent?.description(),
|
description = noteEvent?.description(),
|
||||||
users =
|
users =
|
||||||
accountViewModel
|
accountViewModel
|
||||||
.loadUsersSync(
|
.sortUsersSync(
|
||||||
noteEvent?.followIds() ?: emptyList(),
|
noteEvent?.followIds() ?: emptyList(),
|
||||||
).toImmutableList(),
|
).toImmutableList(),
|
||||||
)
|
)
|
||||||
@@ -119,11 +118,11 @@ fun RenderFollowSetThumbPreview() {
|
|||||||
"https://i.postimg.cc/GtDgGY5v/5062563795762785335.jpg",
|
"https://i.postimg.cc/GtDgGY5v/5062563795762785335.jpg",
|
||||||
"Desc",
|
"Desc",
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
accountViewModel.userProfile(),
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
accountViewModel.userProfile(),
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
accountViewModel.userProfile(),
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
accountViewModel.userProfile(),
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
accountViewModel.userProfile(),
|
accountViewModel.userProfile().pubkeyHex,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
baseNote = Note(""),
|
baseNote = Note(""),
|
||||||
@@ -164,7 +163,7 @@ fun RenderFollowSetThumb(
|
|||||||
)
|
)
|
||||||
} ?: run { DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier) }
|
} ?: run { DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier) }
|
||||||
|
|
||||||
Gallery(card.users, Modifier.padding(Size10dp), accountViewModel, nav)
|
GalleryUnloaded(card.users, StdPadding, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = DoubleVertSpacer)
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|||||||
+313
@@ -0,0 +1,313 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.statusBars
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.TabRow
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
||||||
|
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TitleIconModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarSize
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ReplyReaction
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.UserFeedView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.FollowPackFeedConversationsFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.FollowPackFeedNewThreadFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.FollowPackMembersUserFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource.FollowPackFeedFilterAssemblerSubscription
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HalfHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SpacedBy2dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FollowPackFeedScreen(
|
||||||
|
address: Address,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
LoadAddressableNote(address, accountViewModel) {
|
||||||
|
it?.let {
|
||||||
|
PrepareViewModelsFollowPackScreen(
|
||||||
|
note = it,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("StateFlowValueCalledInComposition")
|
||||||
|
@Composable
|
||||||
|
fun PrepareViewModelsFollowPackScreen(
|
||||||
|
note: AddressableNote,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val conversationsFeedViewModel: FollowPackFeedConversationsFeedViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = note.idHex + "ConversationsFeedViewModel",
|
||||||
|
factory =
|
||||||
|
FollowPackFeedConversationsFeedViewModel.Factory(
|
||||||
|
note,
|
||||||
|
accountViewModel.account,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val newThreadFeedViewModel: FollowPackFeedNewThreadFeedViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = note.idHex + "NewThreadFeedViewModel",
|
||||||
|
factory =
|
||||||
|
FollowPackFeedNewThreadFeedViewModel.Factory(
|
||||||
|
note,
|
||||||
|
accountViewModel.account,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val membersFeedViewModel: FollowPackMembersUserFeedViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = note.idHex + "MembersFeedViewModel",
|
||||||
|
factory =
|
||||||
|
FollowPackMembersUserFeedViewModel.Factory(
|
||||||
|
note,
|
||||||
|
accountViewModel.account,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
FollowPackFeedScreen(note, newThreadFeedViewModel, conversationsFeedViewModel, membersFeedViewModel, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FollowPackFeedScreen(
|
||||||
|
note: AddressableNote,
|
||||||
|
newThreadFeedViewModel: FollowPackFeedNewThreadFeedViewModel,
|
||||||
|
conversationsFeedViewModel: FollowPackFeedConversationsFeedViewModel,
|
||||||
|
membersFeedViewModel: FollowPackMembersUserFeedViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
WatchLifecycleAndUpdateModel(newThreadFeedViewModel)
|
||||||
|
WatchLifecycleAndUpdateModel(conversationsFeedViewModel)
|
||||||
|
WatchLifecycleAndUpdateModel(membersFeedViewModel)
|
||||||
|
|
||||||
|
FollowPackFeedFilterAssemblerSubscription(note, accountViewModel)
|
||||||
|
|
||||||
|
val pagerState = rememberForeverPagerState(note.idHex + "FollowPackScreenPagerState") { 3 }
|
||||||
|
|
||||||
|
DisappearingScaffold(
|
||||||
|
isInvertedLayout = false,
|
||||||
|
topBar = {
|
||||||
|
Column {
|
||||||
|
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||||
|
val modifier = Modifier.fillMaxWidth().height(TopBarSize + statusBarHeight)
|
||||||
|
Box(
|
||||||
|
modifier = modifier, // Adjust height as needed for your banner
|
||||||
|
) {
|
||||||
|
DisplayBanner(note, Modifier.fillMaxSize(), accountViewModel)
|
||||||
|
|
||||||
|
ShorterTopAppBar(
|
||||||
|
title = {
|
||||||
|
FollowPackHeader(note, accountViewModel, nav)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(
|
||||||
|
onClick = nav::popBack,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringRes(R.string.back),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = SpacedBy2dp) {
|
||||||
|
ReplyReaction(
|
||||||
|
baseNote = note,
|
||||||
|
grayTint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
iconSizeModifier = Size18Modifier,
|
||||||
|
) {
|
||||||
|
nav.nav {
|
||||||
|
Route.Note(note.idHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = HalfHorzSpacer)
|
||||||
|
LikeReaction(
|
||||||
|
baseNote = note,
|
||||||
|
grayTint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
Spacer(modifier = HalfHorzSpacer)
|
||||||
|
ZapReaction(
|
||||||
|
baseNote = note,
|
||||||
|
grayTint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
Spacer(modifier = HalfHorzSpacer)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors =
|
||||||
|
TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.6f), // Make TopAppBar background transparent
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TabRow(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
modifier = TabRowHeight,
|
||||||
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 0,
|
||||||
|
text = { Text(text = stringRes(R.string.new_threads)) },
|
||||||
|
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
|
||||||
|
)
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 1,
|
||||||
|
text = { Text(text = stringRes(R.string.conversations)) },
|
||||||
|
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
||||||
|
)
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 2,
|
||||||
|
text = { Text(text = stringRes(R.string.members)) },
|
||||||
|
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
|
HorizontalPager(
|
||||||
|
contentPadding = it,
|
||||||
|
state = pagerState,
|
||||||
|
) { page ->
|
||||||
|
when (page) {
|
||||||
|
0 ->
|
||||||
|
RefresheableFeedView(
|
||||||
|
newThreadFeedViewModel,
|
||||||
|
null,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
1 ->
|
||||||
|
RefresheableFeedView(
|
||||||
|
conversationsFeedViewModel,
|
||||||
|
null,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
2 ->
|
||||||
|
UserFeedView(
|
||||||
|
membersFeedViewModel,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DisplayBanner(
|
||||||
|
baseNote: AddressableNote,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val noteEvent by observeNoteEvent<FollowListEvent>(baseNote, accountViewModel)
|
||||||
|
|
||||||
|
noteEvent?.image()?.let {
|
||||||
|
AsyncImage(
|
||||||
|
model = it,
|
||||||
|
contentDescription = stringRes(R.string.preview_card_image_for, it),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPackHeader(
|
||||||
|
baseNote: AddressableNote,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val noteEvent by observeNoteEvent<FollowListEvent>(baseNote, accountViewModel)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = noteEvent?.title() ?: baseNote.dTag(),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||||
|
import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
|
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||||
|
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||||
|
|
||||||
|
class FollowPackFeedConversationsFeedFilter(
|
||||||
|
val followPackNote: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : AdditiveFeedFilter<Note>() {
|
||||||
|
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultHomeFollowList.value
|
||||||
|
|
||||||
|
override fun showHiddenKey(): Boolean =
|
||||||
|
account.liveHomeFollowLists.value is MutedAuthorsByOutboxTopNavFilter ||
|
||||||
|
account.liveHomeFollowLists.value is MutedAuthorsByProxyTopNavFilter
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val filterParams = buildFilterParams(account)
|
||||||
|
|
||||||
|
return sort(
|
||||||
|
LocalCache.notes.filterIntoSet { _, it ->
|
||||||
|
acceptableEvent(it, filterParams)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val followPackEvent = followPackNote.event as? FollowListEvent
|
||||||
|
val follows = followPackEvent?.followIdSet() ?: emptySet()
|
||||||
|
|
||||||
|
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
||||||
|
|
||||||
|
fun buildFilterParams(account: Account): FilterByListParams =
|
||||||
|
FilterByListParams.create(
|
||||||
|
followLists =
|
||||||
|
if (account.proxyRelayList.flow.value
|
||||||
|
.isEmpty()
|
||||||
|
) {
|
||||||
|
AllUserFollowsByOutboxTopNavFilter(
|
||||||
|
authors = follows,
|
||||||
|
defaultRelays = account.defaultGlobalRelays.flow,
|
||||||
|
blockedRelays = account.blockedRelayList.flow,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AllUserFollowsByProxyTopNavFilter(
|
||||||
|
authors = follows,
|
||||||
|
proxyRelays = account.proxyRelayList.flow.value,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
hiddenUsers = account.hiddenUsers.flow.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
|
val filterParams = buildFilterParams(account)
|
||||||
|
|
||||||
|
return collection.filterTo(HashSet()) {
|
||||||
|
acceptableEvent(it, filterParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun acceptableEvent(
|
||||||
|
event: Event?,
|
||||||
|
filterParams: FilterByListParams,
|
||||||
|
): Boolean =
|
||||||
|
(
|
||||||
|
event is TextNoteEvent ||
|
||||||
|
event is PollNoteEvent ||
|
||||||
|
event is ChannelMessageEvent ||
|
||||||
|
event is CommentEvent ||
|
||||||
|
event is VoiceReplyEvent ||
|
||||||
|
event is PublicMessageEvent ||
|
||||||
|
event is LiveActivitiesChatMessageEvent
|
||||||
|
) &&
|
||||||
|
filterParams.match(event)
|
||||||
|
|
||||||
|
fun acceptableEvent(
|
||||||
|
note: Note,
|
||||||
|
filterParams: FilterByListParams,
|
||||||
|
): Boolean = acceptableEvent(note.event, filterParams) && !note.isNewThread()
|
||||||
|
|
||||||
|
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||||
|
|
||||||
|
class FollowPackFeedConversationsFeedViewModel(
|
||||||
|
val note: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : FeedViewModel(FollowPackFeedConversationsFeedFilter(note, account)) {
|
||||||
|
class Factory(
|
||||||
|
val note: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = FollowPackFeedConversationsFeedViewModel(note, account) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.filterIntoSet
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||||
|
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
|
||||||
|
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
|
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||||
|
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||||
|
|
||||||
|
class FollowPackFeedNewThreadFeedFilter(
|
||||||
|
val followPackNote: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : AdditiveFeedFilter<Note>() {
|
||||||
|
companion object Companion {
|
||||||
|
val ADDRESSABLE_KINDS =
|
||||||
|
listOf(
|
||||||
|
AudioTrackEvent.KIND,
|
||||||
|
InteractiveStoryPrologueEvent.KIND,
|
||||||
|
WikiNoteEvent.KIND,
|
||||||
|
ClassifiedsEvent.KIND,
|
||||||
|
LongTextNoteEvent.KIND,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val followPackEvent = followPackNote.event as? FollowListEvent
|
||||||
|
val follows = followPackEvent?.followIdSet() ?: emptySet()
|
||||||
|
|
||||||
|
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followPackNote.idHex
|
||||||
|
|
||||||
|
override fun showHiddenKey(): Boolean = false
|
||||||
|
|
||||||
|
fun buildFilterParams(account: Account): FilterByListParams =
|
||||||
|
FilterByListParams.create(
|
||||||
|
followLists =
|
||||||
|
if (account.proxyRelayList.flow.value
|
||||||
|
.isEmpty()
|
||||||
|
) {
|
||||||
|
AllUserFollowsByOutboxTopNavFilter(
|
||||||
|
authors = follows,
|
||||||
|
defaultRelays = account.defaultGlobalRelays.flow,
|
||||||
|
blockedRelays = account.blockedRelayList.flow,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AllUserFollowsByProxyTopNavFilter(
|
||||||
|
authors = follows,
|
||||||
|
proxyRelays = account.proxyRelayList.flow.value,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
hiddenUsers = account.hiddenUsers.flow.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val filterParams = buildFilterParams(account)
|
||||||
|
|
||||||
|
val notes =
|
||||||
|
LocalCache.notes.filterIntoSet { _, note ->
|
||||||
|
// Avoids processing addressables twice.
|
||||||
|
(note.event?.kind ?: 99999) < 10000 && acceptableEvent(note, filterParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
val longFormNotes =
|
||||||
|
LocalCache.addressables.filterIntoSet(
|
||||||
|
kinds = ADDRESSABLE_KINDS,
|
||||||
|
) { _, note ->
|
||||||
|
acceptableEvent(note, filterParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sort(notes + longFormNotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
||||||
|
|
||||||
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
|
val filterParams = buildFilterParams(account)
|
||||||
|
|
||||||
|
return collection.filterTo(HashSet()) {
|
||||||
|
acceptableEvent(it, filterParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun acceptableEvent(
|
||||||
|
it: Note,
|
||||||
|
filterParams: FilterByListParams,
|
||||||
|
): Boolean {
|
||||||
|
val noteEvent = it.event
|
||||||
|
return (
|
||||||
|
noteEvent is TextNoteEvent ||
|
||||||
|
noteEvent is ClassifiedsEvent ||
|
||||||
|
noteEvent is RepostEvent ||
|
||||||
|
noteEvent is GenericRepostEvent ||
|
||||||
|
(noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||||
|
(noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||||
|
noteEvent is PollNoteEvent ||
|
||||||
|
noteEvent is HighlightEvent ||
|
||||||
|
noteEvent is InteractiveStoryPrologueEvent ||
|
||||||
|
noteEvent is CommentEvent ||
|
||||||
|
noteEvent is AudioTrackEvent ||
|
||||||
|
noteEvent is VoiceEvent ||
|
||||||
|
noteEvent is AudioHeaderEvent
|
||||||
|
) &&
|
||||||
|
filterParams.match(noteEvent, it.relays) &&
|
||||||
|
it.isNewThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sort(items: Set<Note>): List<Note> =
|
||||||
|
items
|
||||||
|
.distinctBy {
|
||||||
|
if (it.event is RepostEvent || it.event is GenericRepostEvent) {
|
||||||
|
it.replyTo?.lastOrNull()?.idHex ?: it.idHex // only the most recent repost per feed.
|
||||||
|
} else {
|
||||||
|
it.idHex
|
||||||
|
}
|
||||||
|
}.sortedWith(DefaultFeedOrder)
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||||
|
|
||||||
|
class FollowPackFeedNewThreadFeedViewModel(
|
||||||
|
val note: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : FeedViewModel(FollowPackFeedNewThreadFeedFilter(note, account)) {
|
||||||
|
class Factory(
|
||||||
|
val note: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = FollowPackFeedNewThreadFeedViewModel(note, account) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache.checkGetOrCreateUser
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
|
|
||||||
|
class FollowPackMembersFeedFilter(
|
||||||
|
val followPackNote: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : FeedFilter<User>() {
|
||||||
|
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followPackNote.idHex
|
||||||
|
|
||||||
|
val cache: MutableMap<FollowListEvent, List<User>> = mutableMapOf()
|
||||||
|
|
||||||
|
override fun feed(): List<User> {
|
||||||
|
val followPackEvent = followPackNote.event as? FollowListEvent ?: return emptyList()
|
||||||
|
|
||||||
|
val previousList = cache[followPackEvent]
|
||||||
|
if (previousList != null) return previousList
|
||||||
|
|
||||||
|
val follows =
|
||||||
|
followPackEvent
|
||||||
|
.followIdSet()
|
||||||
|
.mapNotNull { hex -> checkGetOrCreateUser(hex) }
|
||||||
|
.filter { !account.isHidden(it) }
|
||||||
|
.sortedByDescending { account.isKnown(it) }
|
||||||
|
|
||||||
|
cache[followPackEvent] = follows
|
||||||
|
return follows
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
||||||
|
|
||||||
|
class FollowPackMembersUserFeedViewModel(
|
||||||
|
val followPackNote: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : UserFeedViewModel(FollowPackMembersFeedFilter(followPackNote, account)) {
|
||||||
|
class Factory(
|
||||||
|
val followPackNote: AddressableNote,
|
||||||
|
val account: Account,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = FollowPackMembersUserFeedViewModel(followPackNote, account) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
|
|
||||||
|
// This allows multiple screen to be listening to tags, even the same tag
|
||||||
|
class FollowPackFeedQueryState(
|
||||||
|
var followPack: AddressableNote,
|
||||||
|
var account: Account,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class FollowPackFeedFilterAssembler(
|
||||||
|
client: INostrClient,
|
||||||
|
) : ComposeSubscriptionManager<FollowPackFeedQueryState>() {
|
||||||
|
val group =
|
||||||
|
listOf(
|
||||||
|
FollowPackFeedFilterSubAssembler(client, ::allKeys),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun invalidateKeys() = invalidateFilters()
|
||||||
|
|
||||||
|
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||||
|
|
||||||
|
override fun destroy() = group.forEach { it.destroy() }
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPackFeedFilterAssemblerSubscription(
|
||||||
|
pack: AddressableNote,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
// different screens get different states
|
||||||
|
// even if they are tracking the same tag.
|
||||||
|
val state =
|
||||||
|
remember(pack) {
|
||||||
|
FollowPackFeedQueryState(pack, accountViewModel.account)
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyDataSourceSubscription(state, accountViewModel.dataSources().followPacks)
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.filterHomePostsByAuthors
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
|
|
||||||
|
class FollowPackFeedFilterSubAssembler(
|
||||||
|
client: INostrClient,
|
||||||
|
allKeys: () -> Set<FollowPackFeedQueryState>,
|
||||||
|
) : SingleSubEoseManager<FollowPackFeedQueryState>(client, allKeys) {
|
||||||
|
override fun updateFilter(
|
||||||
|
keys: List<FollowPackFeedQueryState>,
|
||||||
|
since: SincePerRelayMap?,
|
||||||
|
): List<RelayBasedFilter> {
|
||||||
|
if (keys.isEmpty()) return emptyList()
|
||||||
|
return keys.flatMap {
|
||||||
|
val followPack = it.followPack.event
|
||||||
|
if (followPack is FollowListEvent) {
|
||||||
|
val filter =
|
||||||
|
if (it.account.proxyRelayList.flow.value
|
||||||
|
.isEmpty()
|
||||||
|
) {
|
||||||
|
AllUserFollowsByOutboxTopNavFilter(
|
||||||
|
authors = followPack.followIdSet(),
|
||||||
|
defaultRelays = it.account.defaultGlobalRelays.flow,
|
||||||
|
blockedRelays = it.account.blockedRelayList.flow,
|
||||||
|
).startValue(it.account.cache)
|
||||||
|
} else {
|
||||||
|
AllUserFollowsByProxyTopNavFilter(
|
||||||
|
authors = followPack.followIdSet(),
|
||||||
|
proxyRelays = it.account.proxyRelayList.flow.value,
|
||||||
|
).startValue(it.account.cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
filterHomePostsByAuthors(filter, since, null, null)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun distinct(key: FollowPackFeedQueryState) = key.followPack.idHex
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ import androidx.compose.ui.text.PlaceholderVerticalAlign
|
|||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarSize
|
||||||
|
|
||||||
val Shapes =
|
val Shapes =
|
||||||
Shapes(
|
Shapes(
|
||||||
@@ -82,6 +83,8 @@ val HalfVertSpacer = Modifier.height(2.dp)
|
|||||||
|
|
||||||
val MinHorzSpacer = Modifier.width(1.dp)
|
val MinHorzSpacer = Modifier.width(1.dp)
|
||||||
|
|
||||||
|
val HalfHorzSpacer = Modifier.width(3.dp)
|
||||||
|
|
||||||
val StdHorzSpacer = Modifier.width(5.dp)
|
val StdHorzSpacer = Modifier.width(5.dp)
|
||||||
val StdVertSpacer = Modifier.height(5.dp)
|
val StdVertSpacer = Modifier.height(5.dp)
|
||||||
|
|
||||||
@@ -374,3 +377,5 @@ val SpacedBy10dp = Arrangement.spacedBy(Size10dp)
|
|||||||
val PopupUpEffect = RoundedCornerShape(0.dp, 0.dp, 15.dp, 15.dp)
|
val PopupUpEffect = RoundedCornerShape(0.dp, 0.dp, 15.dp, 15.dp)
|
||||||
|
|
||||||
val Size50ModifierOffset10 = Modifier.size(50.dp).offset(y = (-10).dp)
|
val Size50ModifierOffset10 = Modifier.size(50.dp).offset(y = (-10).dp)
|
||||||
|
|
||||||
|
val FollowPackHeaderModifier = Modifier.fillMaxWidth().height(TopBarSize)
|
||||||
|
|||||||
@@ -273,6 +273,18 @@ val lightNewItemBubbleModifier =
|
|||||||
.clip(shape = CircleShape)
|
.clip(shape = CircleShape)
|
||||||
.background(LightColorPalette.primary)
|
.background(LightColorPalette.primary)
|
||||||
|
|
||||||
|
val darkBlackTagModifier =
|
||||||
|
Modifier
|
||||||
|
.clip(SmallestBorder)
|
||||||
|
.background(DarkColorPalette.onBackground)
|
||||||
|
.padding(horizontal = 5.dp)
|
||||||
|
|
||||||
|
val lightBlackTagModifier =
|
||||||
|
Modifier
|
||||||
|
.clip(SmallestBorder)
|
||||||
|
.background(LightColorPalette.onBackground)
|
||||||
|
.padding(horizontal = 5.dp)
|
||||||
|
|
||||||
val RichTextDefaults = RichTextStyle().resolveDefaults()
|
val RichTextDefaults = RichTextStyle().resolveDefaults()
|
||||||
|
|
||||||
val MarkDownStyleOnDark =
|
val MarkDownStyleOnDark =
|
||||||
@@ -474,6 +486,10 @@ val ColorScheme.largeProfilePictureModifier: Modifier
|
|||||||
val ColorScheme.newItemBubbleModifier: Modifier
|
val ColorScheme.newItemBubbleModifier: Modifier
|
||||||
get() = if (isLight) lightNewItemBubbleModifier else darkNewItemBubbleModifier
|
get() = if (isLight) lightNewItemBubbleModifier else darkNewItemBubbleModifier
|
||||||
|
|
||||||
|
@Suppress("ModifierFactoryExtensionFunction")
|
||||||
|
val ColorScheme.blackTagModifier: Modifier
|
||||||
|
get() = if (isLight) lightBlackTagModifier else darkBlackTagModifier
|
||||||
|
|
||||||
val chartLightColors =
|
val chartLightColors =
|
||||||
VicoTheme(
|
VicoTheme(
|
||||||
candlestickCartesianLayerColors =
|
candlestickCartesianLayerColors =
|
||||||
|
|||||||
@@ -1350,4 +1350,7 @@
|
|||||||
<string name="public_members_count">Public Members (%1$s)</string>
|
<string name="public_members_count">Public Members (%1$s)</string>
|
||||||
<string name="add_user_to_the_list">Add user to the list</string>
|
<string name="add_user_to_the_list">Add user to the list</string>
|
||||||
<string name="remove_user_from_the_list">Add user to the list</string>
|
<string name="remove_user_from_the_list">Add user to the list</string>
|
||||||
|
|
||||||
|
<string name="follow_list_item_label">Follow Pack</string>
|
||||||
|
<string name="members">Members</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user