- Adds support for Follow packs

- Enables top filter by pack as well.
This commit is contained in:
Vitor Pamplona
2025-04-29 17:24:03 -04:00
parent 9df0f6c368
commit 626bfc95cb
26 changed files with 440 additions and 33 deletions
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.Account.Companion.APP_SPECIFIC_DATA_D_TAG
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder
@@ -140,6 +141,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.Response
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
@@ -194,6 +196,7 @@ import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.toSet
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -697,9 +700,11 @@ class Account(
LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
}
} else {
val peopleList = noteState.note.event as? GeneralListEvent
if (peopleList != null) {
waitToDecrypt(peopleList) ?: LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
val noteEvent = noteState.note.event
if (noteEvent is GeneralListEvent) {
waitToDecrypt(noteEvent) ?: LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
} else if (noteEvent is FollowListEvent) {
LiveFollowList(authors = noteEvent.pubKeys().toSet(), authorsPlusMe = setOf(signer.pubKey) + noteEvent.pubKeys())
} else {
LiveFollowList(authorsPlusMe = setOf(signer.pubKey))
}
@@ -3622,10 +3627,15 @@ class Account(
fun getAllPeopleLists(pubkey: HexKey): List<AddressableNote> =
LocalCache.addressables
.filter { _, addressableNote ->
val event = (addressableNote.event as? PeopleListEvent)
event != null &&
event.pubKey == pubkey &&
(event.hasAnyTaggedUser() || event.cachedPrivateTags()?.isNotEmpty() == true)
val noteEvent = addressableNote.event
if (noteEvent is PeopleListEvent) {
noteEvent.pubKey == pubkey && (noteEvent.hasAnyTaggedUser() || noteEvent.cachedPrivateTags()?.isNotEmpty() == true)
} else if (noteEvent is FollowListEvent) {
noteEvent.pubKey == pubkey && noteEvent.hasAnyTaggedUser()
} else {
false
}
}
fun markAsRead(
@@ -109,6 +109,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
@@ -828,6 +829,13 @@ object LocalCache : ILocalCache {
consumeBaseReplaceable(event, relay)
}
fun consume(
event: FollowListEvent,
relay: Relay?,
) {
consumeBaseReplaceable(event, relay)
}
private fun consume(
event: AdvertisedRelayListEvent,
relay: Relay?,
@@ -2622,6 +2630,7 @@ object LocalCache : ILocalCache {
is FileServersEvent -> consume(event, relay)
is FileStorageEvent -> consume(event, relay)
is FileStorageHeaderEvent -> consume(event, relay)
is FollowListEvent -> consume(event, relay)
is GiftWrapEvent -> consume(event, relay)
is GitIssueEvent -> consume(event, relay)
is GitReplyEvent -> consume(event, relay)
@@ -53,6 +53,7 @@ import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
@@ -135,6 +136,7 @@ class AccountFilterAssembler(
kinds =
listOf(
PeopleListEvent.KIND,
FollowListEvent.KIND,
MuteListEvent.KIND,
BadgeProfilesEvent.KIND,
EmojiPackSelectionEvent.KIND,
@@ -55,6 +55,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
@@ -226,6 +227,7 @@ class SearchFilterAssembler(
listOf(
InteractiveStoryPrologueEvent.KIND,
InteractiveStorySceneEvent.KIND,
FollowListEvent.KIND,
),
search = mySearchString,
limit = 100,
@@ -70,6 +70,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import kotlinx.collections.immutable.ImmutableList
@@ -252,7 +253,15 @@ fun RenderOption(option: Name) {
) {
val noteState by observeNote(option.note)
val name = (noteState?.note?.event as? PeopleListEvent)?.nameOrTitle() ?: option.note.dTag() ?: ""
val noteEvent = noteState.note.event
val name =
if (noteEvent is PeopleListEvent) {
noteEvent.nameOrTitle() ?: option.note.dTag()
} else if (noteEvent is FollowListEvent) {
noteEvent.nameOrTitle() ?: option.note.dTag()
} else {
option.note.dTag()
}
Text(text = name, color = MaterialTheme.colorScheme.onSurface)
}
@@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay
import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList
import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList
import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet
@@ -181,6 +182,7 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent
@@ -624,6 +626,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 RelaySetEvent -> DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
is ChatMessageRelayListEvent -> DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav)
is AdvertisedRelayListEvent -> DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
@@ -0,0 +1,134 @@
/**
* Copyright (c) 2024 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.note.types
import androidx.compose.foundation.background
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
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.Modifier
import androidx.compose.ui.graphics.Color
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 com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.getGradient
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@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 name by remember { derivedStateOf { "#${noteEvent.nameOrTitle() ?: noteEvent.dTag()}" } }
Text(
text = name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier
.fillMaxWidth()
.padding(5.dp),
textAlign = TextAlign.Center,
)
LaunchedEffect(Unit) {
accountViewModel.loadUsers(noteEvent.taggedUserIds()) {
members = it
}
}
Box {
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(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier =
Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.background(getGradient(backgroundColor)),
) {
ShowMoreButton { expanded = !expanded }
}
}
}
}
@@ -45,6 +45,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
@@ -135,6 +136,7 @@ class FollowListState(
(
(
noteEvent is PeopleListEvent ||
noteEvent is FollowListEvent ||
noteEvent is MuteListEvent ||
noteEvent is ContactListEvent
) ||
@@ -288,13 +290,22 @@ class ResourceName(
class PeopleListName(
val note: AddressableNote,
) : Name() {
override fun name() = (note.event as? PeopleListEvent)?.nameOrTitle() ?: note.dTag() ?: ""
override fun name(): String {
val noteEvent = note.event
return if (noteEvent is PeopleListEvent) {
noteEvent.nameOrTitle() ?: note.dTag()
} else if (noteEvent is FollowListEvent) {
noteEvent.nameOrTitle() ?: note.dTag()
} else {
note.dTag()
}
}
}
class CommunityName(
val note: AddressableNote,
) : Name() {
override fun name() = "/n/${(note.dTag() ?: "")}"
override fun name() = "/n/${(note.dTag())}"
}
@Immutable
@@ -67,7 +67,7 @@ open class DiscoverChatFeedFilter(
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultDiscoveryFollowList.value,
followLists = account.liveDiscoveryFollowLists.value,
@@ -44,7 +44,7 @@ open class DiscoverCommunityFeedFilter(
override fun feed(): List<Note> {
val filterParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultDiscoveryFollowList.value,
followLists = account.liveDiscoveryFollowLists.value,
@@ -73,7 +73,7 @@ open class DiscoverCommunityFeedFilter(
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
// here, we need to look for CommunityDefinition in new collection AND new CommunityDefinition from Post Approvals
val filterParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultDiscoveryFollowList.value,
followLists = account.liveDiscoveryFollowLists.value,
@@ -55,7 +55,7 @@ open class DiscoverLiveFeedFilter(
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val filterParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultDiscoveryFollowList.value,
followLists = account.liveDiscoveryFollowLists.value,
@@ -55,7 +55,7 @@ open class DiscoverMarketplaceFeedFilter(
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
account.userProfile().pubkeyHex,
account.settings.defaultDiscoveryFollowList.value,
account.liveDiscoveryFollowLists.value,
@@ -56,7 +56,7 @@ open class DiscoverNIP89FeedFilter(
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
account.userProfile().pubkeyHex,
account.settings.defaultDiscoveryFollowList.value,
account.liveDiscoveryFollowLists.value,
@@ -43,8 +43,8 @@ open class NIP90ContentDiscoveryResponseFilter(
open fun followList(): String = account.settings.defaultDiscoveryFollowList.value
override fun showHiddenKey(): Boolean =
followList() == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) ||
followList() == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
fun acceptableEvent(note: Note): Boolean {
val noteEvent = note.event
@@ -72,7 +72,7 @@ open class NIP90ContentDiscoveryResponseFilter(
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
account.userProfile().pubkeyHex,
account.settings.defaultDiscoveryFollowList.value,
account.liveDiscoveryFollowLists.value,
@@ -57,7 +57,7 @@ class HomeConversationsFeedFilter(
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultHomeFollowList.value,
followLists = account.liveHomeFollowLists.value,
@@ -51,7 +51,7 @@ class HomeNewThreadFeedFilter(
account.settings.defaultHomeFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultHomeFollowList.value,
followLists = account.liveHomeFollowLists.value,
@@ -57,12 +57,12 @@ class NotificationFeedFilter(
override fun showHiddenKey(): Boolean =
account.settings.defaultNotificationFollowList.value ==
PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) ||
PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
account.settings.defaultNotificationFollowList.value ==
MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultNotificationFollowList.value,
followLists = account.liveNotificationFollowLists.value,
@@ -41,6 +41,7 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
@@ -179,6 +180,7 @@ class UserProfileFilterAssembler(
listOf(
BookmarkListEvent.KIND,
PeopleListEvent.KIND,
FollowListEvent.KIND,
AppRecommendationEvent.KIND,
),
authors = listOf(state.user.pubkeyHex),
@@ -40,8 +40,8 @@ class UserProfileGalleryFeedFilter(
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + "ProfileGallery"
override fun showHiddenKey(): Boolean =
account.settings.defaultStoriesFollowList.value == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) ||
account.settings.defaultStoriesFollowList.value == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
account.settings.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
account.settings.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
override fun feed(): List<Note> {
val params = buildFilterParams(account)
@@ -77,7 +77,7 @@ class UserProfileGalleryFeedFilter(
}
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultStoriesFollowList.value,
followLists = account.liveStoriesFollowLists.value,
@@ -118,6 +118,7 @@ import com.vitorpamplona.amethyst.ui.note.types.AudioHeader
import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader
import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay
import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList
import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList
import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList
import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet
@@ -196,6 +197,7 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent
@@ -531,6 +533,8 @@ private fun FullBleedNoteCompose(
FileStorageHeaderDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel)
} else if (noteEvent is PeopleListEvent) {
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is FollowListEvent) {
DisplayFollowList(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AudioTrackEvent) {
AudioTrackHeader(noteEvent, baseNote, ContentScale.FillWidth, accountViewModel, nav)
} else if (noteEvent is AudioHeaderEvent) {
@@ -45,8 +45,8 @@ class VideoFeedFilter(
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultStoriesFollowList.value
override fun showHiddenKey(): Boolean =
account.settings.defaultStoriesFollowList.value == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) ||
account.settings.defaultStoriesFollowList.value == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex)
account.settings.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
account.settings.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
override fun feed(): List<Note> {
val params = buildFilterParams(account)
@@ -120,7 +120,7 @@ class VideoFeedFilter(
}
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.Companion.create(
FilterByListParams.create(
userHex = account.userProfile().pubkeyHex,
selectedListName = account.settings.defaultStoriesFollowList.value,
followLists = account.liveStoriesFollowLists.value,
@@ -70,10 +70,12 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
@@ -197,6 +199,7 @@ class EventFactory {
FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
FileStorageHeaderEvent.KIND -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig)
FhirResourceEvent.KIND -> FhirResourceEvent(id, pubKey, createdAt, tags, content, sig)
FollowListEvent.KIND -> FollowListEvent(id, pubKey, createdAt, tags, content, sig)
GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
GiftWrapEvent.KIND -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig)
GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig)
@@ -220,9 +223,7 @@ class EventFactory {
MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig)
NNSEvent.KIND -> NNSEvent(id, pubKey, createdAt, tags, content, sig)
com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent.KIND ->
com.vitorpamplona.quartz.nip46RemoteSigner
.NostrConnectEvent(id, pubKey, createdAt, tags, content, sig)
NostrConnectEvent.KIND -> NostrConnectEvent(id, pubKey, createdAt, tags, content, sig)
NIP90StatusEvent.KIND -> NIP90StatusEvent(id, pubKey, createdAt, tags, content, sig)
NIP90ContentDiscoveryRequestEvent.KIND -> NIP90ContentDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig)
NIP90ContentDiscoveryResponseEvent.KIND -> NIP90ContentDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig)
@@ -0,0 +1,142 @@
/**
* Copyright (c) 2024 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.quartz.nip51Lists
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag.Companion.parse
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class FollowListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun pubKeys() = tags.mapNotNull(PTag::parseKey)
fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
fun name() = tags.firstNotNullOfOrNull(NameTag::parse)
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun nameOrTitle() = name() ?: title()
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
companion object {
const val KIND = 39089
const val ALT = "List of people to follow"
fun createListWithUser(
name: String,
pubKeyHex: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (FollowListEvent) -> Unit,
) {
create(
content = "",
tags = arrayOf(arrayOf("d", name), arrayOf("p", pubKeyHex)),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
fun addUsers(
earlierVersion: FollowListEvent,
listPubKeyHex: List<String>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (FollowListEvent) -> Unit,
) {
create(
content = earlierVersion.content,
tags =
earlierVersion.tags.plus(
listPubKeyHex.map { arrayOf("p", it) },
),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
fun addUser(
earlierVersion: FollowListEvent,
pubKeyHex: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (FollowListEvent) -> Unit,
) = addUsers(earlierVersion, listOf(pubKeyHex), signer, createdAt, onReady)
fun removeUser(
earlierVersion: FollowListEvent,
pubKeyHex: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (FollowListEvent) -> Unit,
) {
create(
content = earlierVersion.content,
tags =
earlierVersion.tags
.filter { it.size > 1 && !(it[0] == "p" && it[1] == pubKeyHex) }
.toTypedArray(),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
fun create(
content: String,
tags: Array<Array<String>>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (FollowListEvent) -> Unit,
) {
val newTags =
if (tags.any { it.size > 1 && it[0] == "alt" }) {
tags
} else {
tags + AltTag.assemble(ALT)
}
signer.sign(createdAt, KIND, newTags, content, onReady)
}
}
}
@@ -34,6 +34,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
import kotlinx.collections.immutable.ImmutableSet
@@ -63,6 +65,10 @@ abstract class GeneralListEvent(
@Deprecated("NIP-51 has deprecated Title. Use name instead", ReplaceWith("name()"))
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun nameOrTitle() = name() ?: title()
fun filterTagList(
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2024 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.quartz.nip51Lists.tags
class DescriptionTag {
companion object {
const val TAG_NAME = "description"
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < 2 || tag[0] != TAG_NAME || tag[1].isEmpty()) return null
return tag[1]
}
@JvmStatic
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2024 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.quartz.nip51Lists.tags
class ImageTag {
companion object {
const val TAG_NAME = "image"
@JvmStatic
fun parse(tag: Array<String>): String? {
if (tag.size < 2 || tag[0] != TAG_NAME || tag[1].isEmpty()) return null
return tag[1]
}
@JvmStatic
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}