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 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 {
|
||||
return note.author?.let { isAcceptable(it) } ?: true &&
|
||||
// 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.communities.datasource.CommunityFilterAssembler
|
||||
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.hashtag.datasource.HashtagFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
||||
@@ -77,6 +78,7 @@ class RelaySubscriptionsCoordinator(
|
||||
val profile = UserProfileFilterAssembler(client)
|
||||
val hashtags = HashtagFilterAssembler(client)
|
||||
val geohashes = GeoHashFilterAssembler(client)
|
||||
val followPacks = FollowPackFeedFilterAssembler(client)
|
||||
|
||||
// active when sending zaps via NWC
|
||||
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.drafts.DraftListScreen
|
||||
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.GeoHashScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
|
||||
@@ -149,6 +150,7 @@ fun AppNavigation(
|
||||
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, 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.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.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.message.ChannelMessageEvent
|
||||
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.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
@@ -65,73 +66,82 @@ fun routeFor(
|
||||
fun routeFor(
|
||||
noteEvent: Event,
|
||||
loggedIn: Account,
|
||||
): Route? {
|
||||
): Route? =
|
||||
if (noteEvent is DraftWrapEvent) {
|
||||
val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent)
|
||||
|
||||
if (innerEvent is IsInPublicChatChannel) {
|
||||
innerEvent.channelId()?.let {
|
||||
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())
|
||||
if (innerEvent != null) {
|
||||
routeForInner(innerEvent, loggedIn)
|
||||
} 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 {
|
||||
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(
|
||||
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 routeForUser(userHex: HexKey): Route.Profile = Route.Profile(userHex)
|
||||
|
||||
fun authorRouteFor(note: Note): Route.Profile? = note.author?.pubkeyHex?.let { Route.Profile(it) }
|
||||
|
||||
fun routeReplyTo(
|
||||
|
||||
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.navigation.routes
|
||||
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavHostController
|
||||
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.nip17Dm.base.ChatroomKey
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -96,7 +98,25 @@ sealed class Route {
|
||||
val kind: Int,
|
||||
val pubKeyHex: HexKey,
|
||||
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(
|
||||
val id: String,
|
||||
|
||||
@@ -708,7 +708,7 @@ private fun RenderNoteRow(
|
||||
is BadgeAwardEvent -> RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is FhirResourceEvent -> RenderFhirResource(baseNote, 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 ChatMessageRelayListEvent -> DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
is AdvertisedRelayListEvent -> DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||
|
||||
@@ -933,7 +933,7 @@ fun ObserveLikeText(
|
||||
inner: @Composable (Int) -> Unit,
|
||||
) {
|
||||
val reactionCount by observeNoteReactionCount(baseNote, accountViewModel)
|
||||
|
||||
println("AABBCC $reactionCount ${baseNote.idHex}")
|
||||
inner(reactionCount)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,139 +21,163 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.ShowMoreButton
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
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.DefaultImageHeaderBackground
|
||||
import com.vitorpamplona.amethyst.ui.note.getGradient
|
||||
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.theme.DividerThickness
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun DisplayFollowList(
|
||||
baseNote: Note,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? FollowListEvent ?: return
|
||||
|
||||
var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) }
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val toMembersShow =
|
||||
if (expanded) {
|
||||
members
|
||||
} else {
|
||||
members.take(3)
|
||||
val card =
|
||||
observeNoteEventAndMap(baseNote, accountViewModel) { event: FollowListEvent? ->
|
||||
if (event == null) {
|
||||
FollowSetCard(
|
||||
name = "",
|
||||
media = "",
|
||||
description = "",
|
||||
users = persistentListOf(),
|
||||
)
|
||||
} else {
|
||||
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()
|
||||
|
||||
image?.let {
|
||||
MyAsyncImage(
|
||||
imageUrl = it,
|
||||
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,
|
||||
RenderFollowSetThumbEmbed(
|
||||
card.value,
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
accountViewModel.loadUsers(noteEvent.taggedUserIds()) {
|
||||
members = it
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun RenderFollowSetThumbEmbed(
|
||||
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 {
|
||||
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
||||
toMembersShow.forEach { user ->
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
UserCompose(
|
||||
user,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
GalleryUnloaded(card.users, StdPadding, accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (members.size > 3 && !expanded) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.background(getGradient(backgroundColor)),
|
||||
) {
|
||||
ShowMoreButton { expanded = !expanded }
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = card.name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -71,7 +72,7 @@ fun DisplayPeopleList(
|
||||
|
||||
var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) }
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val toMembersShow =
|
||||
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> =
|
||||
hexList
|
||||
.mapNotNull { hex -> checkGetOrCreateUser(hex) }
|
||||
.sortedBy { account.isFollowing(it) }
|
||||
.reversed()
|
||||
.sortedByDescending { account.isKnown(it) }
|
||||
|
||||
suspend fun checkVideoIsOnline(videoUrl: String): Boolean =
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -40,12 +39,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.ui.components.MyAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
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.UserPicture
|
||||
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.FollowSetImageModifier
|
||||
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.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -72,7 +71,7 @@ data class FollowSetCard(
|
||||
val name: String,
|
||||
val media: String?,
|
||||
val description: String?,
|
||||
val users: ImmutableList<User>,
|
||||
val users: ImmutableList<HexKey>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -90,7 +89,7 @@ fun RenderFollowSetThumb(
|
||||
description = noteEvent?.description(),
|
||||
users =
|
||||
accountViewModel
|
||||
.loadUsersSync(
|
||||
.sortUsersSync(
|
||||
noteEvent?.followIds() ?: emptyList(),
|
||||
).toImmutableList(),
|
||||
)
|
||||
@@ -119,11 +118,11 @@ fun RenderFollowSetThumbPreview() {
|
||||
"https://i.postimg.cc/GtDgGY5v/5062563795762785335.jpg",
|
||||
"Desc",
|
||||
persistentListOf(
|
||||
accountViewModel.userProfile(),
|
||||
accountViewModel.userProfile(),
|
||||
accountViewModel.userProfile(),
|
||||
accountViewModel.userProfile(),
|
||||
accountViewModel.userProfile(),
|
||||
accountViewModel.userProfile().pubkeyHex,
|
||||
accountViewModel.userProfile().pubkeyHex,
|
||||
accountViewModel.userProfile().pubkeyHex,
|
||||
accountViewModel.userProfile().pubkeyHex,
|
||||
accountViewModel.userProfile().pubkeyHex,
|
||||
),
|
||||
),
|
||||
baseNote = Note(""),
|
||||
@@ -164,7 +163,7 @@ fun RenderFollowSetThumb(
|
||||
)
|
||||
} ?: run { DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier) }
|
||||
|
||||
Gallery(card.users, Modifier.padding(Size10dp), accountViewModel, nav)
|
||||
GalleryUnloaded(card.users, StdPadding, accountViewModel, nav)
|
||||
}
|
||||
|
||||
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.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarSize
|
||||
|
||||
val Shapes =
|
||||
Shapes(
|
||||
@@ -82,6 +83,8 @@ val HalfVertSpacer = Modifier.height(2.dp)
|
||||
|
||||
val MinHorzSpacer = Modifier.width(1.dp)
|
||||
|
||||
val HalfHorzSpacer = Modifier.width(3.dp)
|
||||
|
||||
val StdHorzSpacer = Modifier.width(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 Size50ModifierOffset10 = Modifier.size(50.dp).offset(y = (-10).dp)
|
||||
|
||||
val FollowPackHeaderModifier = Modifier.fillMaxWidth().height(TopBarSize)
|
||||
|
||||
@@ -273,6 +273,18 @@ val lightNewItemBubbleModifier =
|
||||
.clip(shape = CircleShape)
|
||||
.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 MarkDownStyleOnDark =
|
||||
@@ -474,6 +486,10 @@ val ColorScheme.largeProfilePictureModifier: Modifier
|
||||
val ColorScheme.newItemBubbleModifier: Modifier
|
||||
get() = if (isLight) lightNewItemBubbleModifier else darkNewItemBubbleModifier
|
||||
|
||||
@Suppress("ModifierFactoryExtensionFunction")
|
||||
val ColorScheme.blackTagModifier: Modifier
|
||||
get() = if (isLight) lightBlackTagModifier else darkBlackTagModifier
|
||||
|
||||
val chartLightColors =
|
||||
VicoTheme(
|
||||
candlestickCartesianLayerColors =
|
||||
|
||||
@@ -1350,4 +1350,7 @@
|
||||
<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="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>
|
||||
|
||||
Reference in New Issue
Block a user