- Refactors people lists

- Adds people and follow list cache on the account class
- Updates TopNav to use the new caching
- Removes redundant viewModels for list feeds
- Adds the ability to add and remove users from the List screen
This commit is contained in:
Vitor Pamplona
2025-11-04 19:24:24 -05:00
parent 50b551bb80
commit be804092d7
45 changed files with 2081 additions and 2024 deletions
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.fastAny
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
@@ -73,9 +74,11 @@ class PeopleListEvent(
fun countMutes() = tags.count(MuteTag::isTagged)
fun publicPeople(): List<MuteTag> = tags.mapNotNull(MuteTag::parse)
fun publicMembers(): List<MuteTag> = tags.mapNotNull(MuteTag::parse)
suspend fun privatePeople(signer: NostrSigner): List<MuteTag>? = privateTags(signer)?.mapNotNull(MuteTag::parse)
fun publicUsersIdSet(): Set<HexKey> = tags.mapNotNullTo(mutableSetOf(), UserTag::parseKey)
suspend fun privateMembers(signer: NostrSigner): List<MuteTag>? = privateTags(signer)?.mapNotNull(MuteTag::parse)
companion object {
const val KIND = 30000
@@ -98,8 +101,8 @@ class PeopleListEvent(
if (isPrivate) {
create(
name = name,
publicPeople = emptyList(),
privatePeople = listOf(person),
publicMembers = emptyList(),
privateMembers = listOf(person),
signer = signer,
dTag = dTag,
createdAt = createdAt,
@@ -107,8 +110,8 @@ class PeopleListEvent(
} else {
create(
name = name,
publicPeople = listOf(person),
privatePeople = emptyList(),
publicMembers = listOf(person),
privateMembers = emptyList(),
signer = signer,
dTag = dTag,
createdAt = createdAt,
@@ -139,6 +142,30 @@ class PeopleListEvent(
)
}
suspend fun addFirst(
earlierVersion: PeopleListEvent,
person: UserTag,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): PeopleListEvent =
if (isPrivate) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
resign(
publicTags = earlierVersion.tags,
privateTags = arrayOf(person.toTagArray()) + privateTags,
signer = signer,
createdAt = createdAt,
)
} else {
resign(
content = earlierVersion.content,
tags = arrayOf(person.toTagArray()) + earlierVersion.tags,
signer = signer,
createdAt = createdAt,
)
}
suspend fun remove(
earlierVersion: PeopleListEvent,
person: MuteTag,
@@ -211,34 +238,34 @@ class PeopleListEvent(
@OptIn(ExperimentalUuidApi::class)
suspend fun create(
name: String,
publicPeople: List<UserTag> = emptyList(),
privatePeople: List<UserTag> = emptyList(),
publicMembers: List<UserTag> = emptyList(),
privateMembers: List<UserTag> = emptyList(),
signer: NostrSigner,
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
): PeopleListEvent {
val template = build(name, publicPeople, privatePeople, signer, dTag, createdAt)
val template = build(name, publicMembers, privateMembers, signer, dTag, createdAt)
return signer.sign(template)
}
@OptIn(ExperimentalUuidApi::class)
suspend fun build(
name: String,
publicPeople: List<UserTag> = emptyList(),
privatePeople: List<UserTag> = emptyList(),
publicMembers: List<UserTag> = emptyList(),
privateMembers: List<UserTag> = emptyList(),
signer: NostrSigner,
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<PeopleListEvent>.() -> Unit = {},
) = eventTemplate<PeopleListEvent>(
kind = KIND,
description = PrivateTagsInContent.encryptNip44(privatePeople.map { it.toTagArray() }.toTypedArray(), signer),
description = PrivateTagsInContent.encryptNip44(privateMembers.map { it.toTagArray() }.toTypedArray(), signer),
createdAt = createdAt,
) {
dTag(dTag)
alt(ALT)
name(name)
peoples(publicPeople)
peoples(publicMembers)
initializer()
}
@@ -247,53 +274,39 @@ class PeopleListEvent(
dTag: String,
title: String,
description: String? = null,
isPrivate: Boolean,
firstPublicMembers: List<String> = emptyList(),
firstPrivateMembers: List<String> = emptyList(),
publicMembers: List<UserTag> = emptyList(),
privateMembers: List<UserTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
): PeopleListEvent {
val newListTemplate =
build(
name = title,
publicPeople =
if (!isPrivate && firstPublicMembers.isNotEmpty()) {
firstPublicMembers.map { UserTag(pubKey = it) }
} else {
emptyList()
},
privatePeople =
if (isPrivate && firstPrivateMembers.isNotEmpty()) {
firstPrivateMembers.map { UserTag(pubKey = it) }
} else {
emptyList()
},
publicMembers = publicMembers,
privateMembers = privateMembers,
signer = signer,
dTag = dTag,
createdAt = createdAt,
) {
if (description != null) addUnique(DescriptionTag.assemble(description))
}
val newList = signer.sign(newListTemplate)
onReady(newList)
return signer.sign(newListTemplate)
}
suspend fun copy(
dTag: String,
title: String,
description: String? = null,
firstPublicMembers: List<String> = emptyList(),
firstPrivateMembers: List<String> = emptyList(),
publicMembers: List<UserTag> = emptyList(),
privateMembers: List<UserTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
): PeopleListEvent {
val cloneTemplate =
build(
name = title,
publicPeople = firstPublicMembers.map { UserTag(pubKey = it) },
privatePeople = firstPrivateMembers.map { UserTag(pubKey = it) },
publicMembers = publicMembers,
privateMembers = privateMembers,
signer = signer,
dTag = dTag,
createdAt = createdAt,
@@ -301,8 +314,7 @@ class PeopleListEvent(
if (description != null) addUnique(DescriptionTag.assemble(description))
}
val listClone = signer.sign(cloneTemplate)
onReady(listClone)
return signer.sign(cloneTemplate)
}
suspend fun createListWithUser(
@@ -311,37 +323,46 @@ class PeopleListEvent(
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
val newList =
create(
name = name,
person = UserTag(pubKey = pubKeyHex),
isPrivate = isPrivate,
signer = signer,
createdAt = createdAt,
)
onReady(newList)
}
): PeopleListEvent =
create(
name = name,
person = UserTag(pubKey = pubKeyHex),
isPrivate = isPrivate,
signer = signer,
createdAt = createdAt,
)
suspend fun addUser(
earlierVersion: PeopleListEvent,
pubKeyHex: String,
relayHint: NormalizedRelayUrl?,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
val newList =
add(
earlierVersion = earlierVersion,
person = UserTag(pubKey = pubKeyHex),
isPrivate = isPrivate,
signer = signer,
createdAt = createdAt,
)
onReady(newList)
}
): PeopleListEvent =
add(
earlierVersion = earlierVersion,
person = UserTag(pubKey = pubKeyHex, relayHint),
isPrivate = isPrivate,
signer = signer,
createdAt = createdAt,
)
suspend fun addUserFirst(
earlierVersion: PeopleListEvent,
pubKeyHex: String,
relayHint: NormalizedRelayUrl?,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): PeopleListEvent =
addFirst(
earlierVersion = earlierVersion,
person = UserTag(pubKey = pubKeyHex, relayHint),
isPrivate = isPrivate,
signer = signer,
createdAt = createdAt,
)
suspend fun removeUser(
earlierVersion: PeopleListEvent,
@@ -349,44 +370,36 @@ class PeopleListEvent(
isUserPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
val updatedList =
remove(
earlierVersion = earlierVersion,
person = UserTag(pubKey = pubKeyHex),
isPrivate = isUserPrivate,
signer = signer,
createdAt = createdAt,
)
onReady(updatedList)
}
): PeopleListEvent =
remove(
earlierVersion = earlierVersion,
person = UserTag(pubKey = pubKeyHex),
isPrivate = isUserPrivate,
signer = signer,
createdAt = createdAt,
)
suspend fun modifyListName(
earlierVersion: PeopleListEvent,
newName: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit = {},
) {
): PeopleListEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val currentTitle = earlierVersion.tags.first { it[0] == NameTag.TAG_NAME || it[0] == TitleTag.TAG_NAME }
val newTitleTag =
if (currentTitle[0] == NameTag.TAG_NAME) {
NameTag.assemble(newName)
} else {
com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
.assemble(newName)
TitleTag.assemble(newName)
}
val modified =
resign(
publicTags = earlierVersion.tags.replaceAll(currentTitle, newTitleTag),
privateTags = privateTags.replaceAll(currentTitle, newTitleTag),
signer = signer,
createdAt = createdAt,
)
onReady(modified)
return resign(
publicTags = earlierVersion.tags.replaceAll(currentTitle, newTitleTag),
privateTags = privateTags.replaceAll(currentTitle, newTitleTag),
signer = signer,
createdAt = createdAt,
)
}
suspend fun modifyDescription(
@@ -394,43 +407,38 @@ class PeopleListEvent(
newDescription: String?,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit = {},
) {
): PeopleListEvent? {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val currentDescriptionTag = earlierVersion.tags.firstOrNull { it[0] == DescriptionTag.TAG_NAME }
val currentDescription = currentDescriptionTag?.get(1)
if (currentDescription.equals(newDescription)) {
// Do nothing
return
return null
} else {
if (newDescription == null || newDescription.isEmpty()) {
val modified =
return resign(
publicTags = earlierVersion.tags.remove { it[0] == DescriptionTag.TAG_NAME },
privateTags = privateTags.remove { it[0] == DescriptionTag.TAG_NAME },
signer = signer,
createdAt = createdAt,
)
} else {
val newDescriptionTag = DescriptionTag.assemble(newDescription)
return if (currentDescriptionTag == null) {
resign(
publicTags = earlierVersion.tags.remove { it[0] == DescriptionTag.TAG_NAME },
privateTags = privateTags.remove { it[0] == DescriptionTag.TAG_NAME },
publicTags = earlierVersion.tags.plusElement(newDescriptionTag),
privateTags = privateTags,
signer = signer,
createdAt = createdAt,
)
onReady(modified)
} else {
val newDescriptionTag = DescriptionTag.assemble(newDescription)
val modified =
if (currentDescriptionTag == null) {
resign(
publicTags = earlierVersion.tags.plusElement(newDescriptionTag),
privateTags = privateTags,
signer = signer,
createdAt = createdAt,
)
} else {
resign(
publicTags = earlierVersion.tags.replaceAll(currentDescriptionTag, newDescriptionTag),
privateTags = privateTags.replaceAll(currentDescriptionTag, newDescriptionTag),
signer = signer,
createdAt = createdAt,
)
}
onReady(modified)
} else {
resign(
publicTags = earlierVersion.tags.replaceAll(currentDescriptionTag, newDescriptionTag),
privateTags = privateTags.replaceAll(currentDescriptionTag, newDescriptionTag),
signer = signer,
createdAt = createdAt,
)
}
}
}
}