Merge remote-tracking branch 'origin/labeled-bookmarks' into labeled-bookmarks
This commit is contained in:
@@ -176,7 +176,6 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
|||||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
||||||
@@ -352,6 +351,7 @@ class Account(
|
|||||||
val liveHomeFollowLists: StateFlow<IFeedTopNavFilter> =
|
val liveHomeFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||||
FeedTopNavFilterState(
|
FeedTopNavFilterState(
|
||||||
feedFilterListName = settings.defaultHomeFollowList,
|
feedFilterListName = settings.defaultHomeFollowList,
|
||||||
|
kind3Follows = kind3FollowList.flow,
|
||||||
allFollows = allFollows.flow,
|
allFollows = allFollows.flow,
|
||||||
locationFlow = geolocationFlow,
|
locationFlow = geolocationFlow,
|
||||||
followsRelays = defaultGlobalRelays.flow,
|
followsRelays = defaultGlobalRelays.flow,
|
||||||
@@ -367,6 +367,7 @@ class Account(
|
|||||||
val liveStoriesFollowLists: StateFlow<IFeedTopNavFilter> =
|
val liveStoriesFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||||
FeedTopNavFilterState(
|
FeedTopNavFilterState(
|
||||||
feedFilterListName = settings.defaultStoriesFollowList,
|
feedFilterListName = settings.defaultStoriesFollowList,
|
||||||
|
kind3Follows = kind3FollowList.flow,
|
||||||
allFollows = allFollows.flow,
|
allFollows = allFollows.flow,
|
||||||
locationFlow = geolocationFlow,
|
locationFlow = geolocationFlow,
|
||||||
followsRelays = defaultGlobalRelays.flow,
|
followsRelays = defaultGlobalRelays.flow,
|
||||||
@@ -382,6 +383,7 @@ class Account(
|
|||||||
val liveDiscoveryFollowLists: StateFlow<IFeedTopNavFilter> =
|
val liveDiscoveryFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||||
FeedTopNavFilterState(
|
FeedTopNavFilterState(
|
||||||
feedFilterListName = settings.defaultDiscoveryFollowList,
|
feedFilterListName = settings.defaultDiscoveryFollowList,
|
||||||
|
kind3Follows = kind3FollowList.flow,
|
||||||
allFollows = allFollows.flow,
|
allFollows = allFollows.flow,
|
||||||
locationFlow = geolocationFlow,
|
locationFlow = geolocationFlow,
|
||||||
followsRelays = defaultGlobalRelays.flow,
|
followsRelays = defaultGlobalRelays.flow,
|
||||||
@@ -397,6 +399,7 @@ class Account(
|
|||||||
val liveNotificationFollowLists: StateFlow<IFeedTopNavFilter> =
|
val liveNotificationFollowLists: StateFlow<IFeedTopNavFilter> =
|
||||||
FeedTopNavFilterState(
|
FeedTopNavFilterState(
|
||||||
feedFilterListName = settings.defaultNotificationFollowList,
|
feedFilterListName = settings.defaultNotificationFollowList,
|
||||||
|
kind3Follows = kind3FollowList.flow,
|
||||||
allFollows = allFollows.flow,
|
allFollows = allFollows.flow,
|
||||||
locationFlow = geolocationFlow,
|
locationFlow = geolocationFlow,
|
||||||
followsRelays = defaultGlobalRelays.flow,
|
followsRelays = defaultGlobalRelays.flow,
|
||||||
@@ -1468,18 +1471,6 @@ class Account(
|
|||||||
val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) }
|
val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) }
|
||||||
|
|
||||||
mine.forEach { giftWrap ->
|
mine.forEach { giftWrap ->
|
||||||
val gift = giftWrap.unwrapOrNull(signer)
|
|
||||||
if (gift is SealedRumorEvent) {
|
|
||||||
val rumor = gift.unsealOrNull(signer)
|
|
||||||
if (rumor != null) {
|
|
||||||
cache.justConsumeMyOwnEvent(rumor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gift != null) {
|
|
||||||
cache.justConsumeMyOwnEvent(gift)
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.justConsumeMyOwnEvent(giftWrap)
|
cache.justConsumeMyOwnEvent(giftWrap)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1693,6 +1684,10 @@ class Account(
|
|||||||
|
|
||||||
fun isFollowing(user: HexKey): Boolean = user in followingKeySet()
|
fun isFollowing(user: HexKey): Boolean = user in followingKeySet()
|
||||||
|
|
||||||
|
fun isKnown(user: User): Boolean = user.pubkeyHex in allFollows.flow.value.authors
|
||||||
|
|
||||||
|
fun isKnown(user: HexKey): Boolean = user in allFollows.flow.value.authors
|
||||||
|
|
||||||
fun isAcceptable(note: Note): Boolean {
|
fun isAcceptable(note: Note): Boolean {
|
||||||
return note.author?.let { isAcceptable(it) } ?: true &&
|
return note.author?.let { isAcceptable(it) } ?: true &&
|
||||||
// if user hasn't hided this author
|
// if user hasn't hided this author
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ val ALL_FOLLOWS = " All Follows "
|
|||||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||||
val ALL_USER_FOLLOWS = " All User Follows "
|
val ALL_USER_FOLLOWS = " All User Follows "
|
||||||
|
|
||||||
|
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||||
|
val KIND3_FOLLOWS = " Main User Follows "
|
||||||
|
|
||||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||||
val AROUND_ME = " Around Me "
|
val AROUND_ME = " Around Me "
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ fun RenderHashTagIconsPreview() {
|
|||||||
) { paragraph, state, spaceWidth, modifier ->
|
) { paragraph, state, spaceWidth, modifier ->
|
||||||
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
||||||
when (word) {
|
when (word) {
|
||||||
is HashTagSegment -> HashTag(word, EmptyNav)
|
is HashTagSegment -> HashTag(word, EmptyNav())
|
||||||
is RegularTextSegment -> Text(word.segmentText)
|
is RegularTextSegment -> Text(word.segmentText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+172
-2
@@ -20,6 +20,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.model.nip51Lists.peopleList
|
package com.vitorpamplona.amethyst.model.nip51Lists.peopleList
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
@@ -30,8 +32,20 @@ import com.vitorpamplona.amethyst.model.filter
|
|||||||
import com.vitorpamplona.amethyst.model.updateFlow
|
import com.vitorpamplona.amethyst.model.updateFlow
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.update
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.description
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.image
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.person
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.personFirst
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.removePerson
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.title
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
|
||||||
import com.vitorpamplona.quartz.utils.flattenToSet
|
import com.vitorpamplona.quartz.utils.flattenToSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -46,6 +60,7 @@ import kotlinx.coroutines.flow.onStart
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maintains several stateflows for each step in processing PeopleLists
|
* Maintains several stateflows for each step in processing PeopleLists
|
||||||
@@ -59,7 +74,7 @@ class FollowListsState(
|
|||||||
) {
|
) {
|
||||||
val user = cache.getOrCreateUser(signer.pubKey)
|
val user = cache.getOrCreateUser(signer.pubKey)
|
||||||
|
|
||||||
fun existingPeopleListNotes() = cache.addressables.filter(FollowListEvent.Companion.KIND, user.pubkeyHex)
|
fun existingPeopleListNotes() = cache.addressables.filter(FollowListEvent.KIND, user.pubkeyHex)
|
||||||
|
|
||||||
val followListVersions = MutableStateFlow(0)
|
val followListVersions = MutableStateFlow(0)
|
||||||
|
|
||||||
@@ -83,7 +98,7 @@ class FollowListsState(
|
|||||||
.transformLatest { emitAll(it.updateFlow<FollowListEvent>()) }
|
.transformLatest { emitAll(it.updateFlow<FollowListEvent>()) }
|
||||||
.onStart { emit(followListNotes.value.events()) }
|
.onStart { emit(followListNotes.value.events()) }
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
fun List<FollowListEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet()
|
fun List<FollowListEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet()
|
||||||
|
|
||||||
@@ -99,6 +114,7 @@ class FollowListsState(
|
|||||||
identifierTag = this.dTag(),
|
identifierTag = this.dTag(),
|
||||||
title = this.title() ?: this.dTag(),
|
title = this.title() ?: this.dTag(),
|
||||||
description = this.description(),
|
description = this.description(),
|
||||||
|
image = this.image(),
|
||||||
privateMembers = emptySet(),
|
privateMembers = emptySet(),
|
||||||
publicMembers = cache.load(this.followIdSet()),
|
publicMembers = cache.load(this.followIdSet()),
|
||||||
)
|
)
|
||||||
@@ -112,6 +128,18 @@ class FollowListsState(
|
|||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
.stateIn(scope, SharingStarted.Companion.Eagerly, emptyList())
|
||||||
|
|
||||||
|
fun List<PeopleList>.select(dTag: String) =
|
||||||
|
this.firstOrNull {
|
||||||
|
it.identifierTag == dTag
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectList(dTag: String) = uiListFlow.value.select(dTag)
|
||||||
|
|
||||||
|
fun selectListFlow(dTag: String) =
|
||||||
|
uiListFlow
|
||||||
|
.map { it.select(dTag) }
|
||||||
|
.onStart { emit(selectList(dTag)) }
|
||||||
|
|
||||||
fun isUserInFollowSets(user: User): Boolean = allPeopleListProfiles.value.contains(user.pubkeyHex)
|
fun isUserInFollowSets(user: User): Boolean = allPeopleListProfiles.value.contains(user.pubkeyHex)
|
||||||
|
|
||||||
fun DeletionEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(FollowListEvent.Companion.KIND) || deletesAnyEventIn(followListsEventIds.value)
|
fun DeletionEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(FollowListEvent.Companion.KIND) || deletesAnyEventIn(followListsEventIds.value)
|
||||||
@@ -140,4 +168,146 @@ class FollowListsState(
|
|||||||
fun forceRefresh() {
|
fun forceRefresh() {
|
||||||
followListVersions.update { it + 1 }
|
followListVersions.update { it + 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------
|
||||||
|
// Updating Lists
|
||||||
|
// --------------
|
||||||
|
|
||||||
|
fun getPeopleListNote(noteIdentifier: String): AddressableNote? = existingPeopleListNotes().find { it.dTag() == noteIdentifier }
|
||||||
|
|
||||||
|
fun getPeopleList(noteIdentifier: String): FollowListEvent = getPeopleListNote(noteIdentifier)?.event as FollowListEvent
|
||||||
|
|
||||||
|
fun User.toUserTag() = UserTag(this.pubkeyHex, this.bestRelayHint())
|
||||||
|
|
||||||
|
fun Set<User>.toUserTags() = map { it.toUserTag() }
|
||||||
|
|
||||||
|
suspend fun addFollowList(
|
||||||
|
name: String,
|
||||||
|
desc: String?,
|
||||||
|
image: String?,
|
||||||
|
member: User? = null,
|
||||||
|
isPrivate: Boolean = false,
|
||||||
|
account: Account,
|
||||||
|
): String {
|
||||||
|
val dTag = UUID.randomUUID().toString()
|
||||||
|
|
||||||
|
val newListTemplate =
|
||||||
|
FollowListEvent.build(
|
||||||
|
name = name,
|
||||||
|
people = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||||
|
dTag = dTag,
|
||||||
|
) {
|
||||||
|
if (desc != null) description(desc)
|
||||||
|
if (image != null) image(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newList = signer.sign(newListTemplate)
|
||||||
|
|
||||||
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
|
return dTag
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateMetadata(
|
||||||
|
name: String?,
|
||||||
|
desc: String?,
|
||||||
|
image: String?,
|
||||||
|
peopleList: PeopleList,
|
||||||
|
account: Account,
|
||||||
|
) {
|
||||||
|
val listEvent = getPeopleList(peopleList.identifierTag)
|
||||||
|
|
||||||
|
val template =
|
||||||
|
listEvent.update {
|
||||||
|
if (name != null) title(name)
|
||||||
|
if (desc != null) description(desc)
|
||||||
|
if (image != null) image(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newList = signer.sign(template)
|
||||||
|
|
||||||
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun cloneFollowSet(
|
||||||
|
currentFollowPack: PeopleList,
|
||||||
|
customCloneName: String?,
|
||||||
|
customCloneDescription: String?,
|
||||||
|
account: Account,
|
||||||
|
) {
|
||||||
|
val listEvent = getPeopleList(currentFollowPack.identifierTag)
|
||||||
|
|
||||||
|
val template =
|
||||||
|
listEvent.update {
|
||||||
|
// new list
|
||||||
|
dTag(UUID.randomUUID().toString())
|
||||||
|
|
||||||
|
// updates names
|
||||||
|
if (customCloneName != null) title(customCloneName)
|
||||||
|
if (customCloneDescription != null) description(customCloneDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newList = signer.sign(template)
|
||||||
|
|
||||||
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteFollowSet(
|
||||||
|
identifierTag: String,
|
||||||
|
account: Account,
|
||||||
|
) {
|
||||||
|
val followListEvent = getPeopleList(identifierTag)
|
||||||
|
val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent)))
|
||||||
|
account.sendMyPublicAndPrivateOutbox(deletionEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun addUserToSet(
|
||||||
|
user: User,
|
||||||
|
identifierTag: String,
|
||||||
|
account: Account,
|
||||||
|
) {
|
||||||
|
val followListEvent = getPeopleList(identifierTag)
|
||||||
|
|
||||||
|
val template =
|
||||||
|
followListEvent.update {
|
||||||
|
person(user.pubkeyHex, user.bestRelayHint())
|
||||||
|
}
|
||||||
|
|
||||||
|
val newList = signer.sign(template)
|
||||||
|
|
||||||
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun addUserFirstToSet(
|
||||||
|
user: User,
|
||||||
|
identifierTag: String,
|
||||||
|
account: Account,
|
||||||
|
) {
|
||||||
|
val followListEvent = getPeopleList(identifierTag)
|
||||||
|
|
||||||
|
val template =
|
||||||
|
followListEvent.update {
|
||||||
|
personFirst(user.pubkeyHex, user.bestRelayHint())
|
||||||
|
}
|
||||||
|
|
||||||
|
val newList = signer.sign(template)
|
||||||
|
|
||||||
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removeUserFromSet(
|
||||||
|
user: User,
|
||||||
|
identifierTag: String,
|
||||||
|
account: Account,
|
||||||
|
) {
|
||||||
|
val followListEvent = getPeopleList(identifierTag)
|
||||||
|
|
||||||
|
val template =
|
||||||
|
followListEvent.update {
|
||||||
|
removePerson(user.pubkeyHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newList = signer.sign(template)
|
||||||
|
|
||||||
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -30,6 +30,7 @@ data class PeopleList(
|
|||||||
val identifierTag: String,
|
val identifierTag: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
|
val image: String?,
|
||||||
val privateMembers: Set<User> = emptySet(),
|
val privateMembers: Set<User> = emptySet(),
|
||||||
val publicMembers: Set<User> = emptySet(),
|
val publicMembers: Set<User> = emptySet(),
|
||||||
) {
|
) {
|
||||||
|
|||||||
+44
-39
@@ -32,9 +32,14 @@ import com.vitorpamplona.amethyst.model.filter
|
|||||||
import com.vitorpamplona.amethyst.model.updateFlow
|
import com.vitorpamplona.amethyst.model.updateFlow
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.update
|
||||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.description
|
||||||
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
|
||||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
|
||||||
import com.vitorpamplona.quartz.utils.flattenToSet
|
import com.vitorpamplona.quartz.utils.flattenToSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -116,6 +121,7 @@ class PeopleListsState(
|
|||||||
identifierTag = this.dTag(),
|
identifierTag = this.dTag(),
|
||||||
title = this.nameOrTitle() ?: this.dTag(),
|
title = this.nameOrTitle() ?: this.dTag(),
|
||||||
description = this.description(),
|
description = this.description(),
|
||||||
|
image = this.image(),
|
||||||
privateMembers = cache.load(decryptionCache.privateUserIdSet(this)),
|
privateMembers = cache.load(decryptionCache.privateUserIdSet(this)),
|
||||||
publicMembers = cache.load(this.publicUsersIdSet()),
|
publicMembers = cache.load(this.publicUsersIdSet()),
|
||||||
)
|
)
|
||||||
@@ -129,17 +135,17 @@ class PeopleListsState(
|
|||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
fun selectListFlow(selectedDTag: String) =
|
fun List<PeopleList>.select(dTag: String) =
|
||||||
|
this.firstOrNull {
|
||||||
|
it.identifierTag == dTag
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectList(dTag: String) = uiListFlow.value.select(dTag)
|
||||||
|
|
||||||
|
fun selectListFlow(dTag: String) =
|
||||||
uiListFlow
|
uiListFlow
|
||||||
.map { peopleLists ->
|
.map { it.select(dTag) }
|
||||||
peopleLists.firstOrNull { list ->
|
.onStart { emit(selectList(dTag)) }
|
||||||
list.identifierTag == selectedDTag
|
|
||||||
}
|
|
||||||
}.onStart {
|
|
||||||
emit(
|
|
||||||
uiListFlow.value.firstOrNull { it.identifierTag == selectedDTag },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun DeletionEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(PeopleListEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value)
|
fun DeletionEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(PeopleListEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value)
|
||||||
|
|
||||||
@@ -183,49 +189,48 @@ class PeopleListsState(
|
|||||||
suspend fun addFollowList(
|
suspend fun addFollowList(
|
||||||
listName: String,
|
listName: String,
|
||||||
listDescription: String?,
|
listDescription: String?,
|
||||||
|
listImage: String?,
|
||||||
member: User? = null,
|
member: User? = null,
|
||||||
isPrivate: Boolean = false,
|
isPrivate: Boolean = false,
|
||||||
account: Account,
|
account: Account,
|
||||||
) {
|
): String {
|
||||||
val newList =
|
val dTag = UUID.randomUUID().toString()
|
||||||
PeopleListEvent.createListWithDescription(
|
val newListTemplate =
|
||||||
dTag = UUID.randomUUID().toString(),
|
PeopleListEvent.build(
|
||||||
title = listName,
|
dTag = dTag,
|
||||||
description = listDescription,
|
name = listName,
|
||||||
publicMembers = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
publicMembers = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||||
privateMembers = if (isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
privateMembers = if (isPrivate && member != null) listOf(member.toUserTag()) else emptyList(),
|
||||||
signer = account.signer,
|
signer = account.signer,
|
||||||
)
|
) {
|
||||||
account.sendMyPublicAndPrivateOutbox(newList)
|
if (listDescription != null) description(listDescription)
|
||||||
|
if (listImage != null) image(listImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun renameFollowList(
|
val newList = signer.sign(newListTemplate)
|
||||||
newName: String,
|
|
||||||
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
|
return dTag
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateMetadata(
|
||||||
|
listName: String?,
|
||||||
|
listDescription: String?,
|
||||||
|
listImage: String?,
|
||||||
peopleList: PeopleList,
|
peopleList: PeopleList,
|
||||||
account: Account,
|
account: Account,
|
||||||
) {
|
) {
|
||||||
val listEvent = getPeopleList(peopleList.identifierTag)
|
val listEvent = getPeopleList(peopleList.identifierTag)
|
||||||
val newList =
|
|
||||||
PeopleListEvent.modifyListName(
|
val template =
|
||||||
earlierVersion = listEvent,
|
listEvent.update {
|
||||||
newName = newName,
|
if (listName != null) name(listName)
|
||||||
signer = account.signer,
|
if (listDescription != null) description(listDescription)
|
||||||
)
|
if (listImage != null) image(listImage)
|
||||||
account.sendMyPublicAndPrivateOutbox(newList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun modifyFollowSetDescription(
|
val newList = signer.sign(template)
|
||||||
newDescription: String?,
|
|
||||||
peopleList: PeopleList,
|
|
||||||
account: Account,
|
|
||||||
) {
|
|
||||||
val listEvent = getPeopleList(peopleList.identifierTag)
|
|
||||||
val newList =
|
|
||||||
PeopleListEvent.modifyDescription(
|
|
||||||
earlierVersion = listEvent,
|
|
||||||
newDescription = newDescription,
|
|
||||||
signer = account.signer,
|
|
||||||
)
|
|
||||||
account.sendMyPublicAndPrivateOutbox(newList)
|
account.sendMyPublicAndPrivateOutbox(newList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -74,7 +74,7 @@ class BlossomServerListState(
|
|||||||
return if (serverList != null && serverList.tags.isNotEmpty()) {
|
return if (serverList != null && serverList.tags.isNotEmpty()) {
|
||||||
BlossomServersEvent.updateRelayList(
|
BlossomServersEvent.updateRelayList(
|
||||||
earlierVersion = serverList,
|
earlierVersion = serverList,
|
||||||
relays = servers,
|
servers = servers,
|
||||||
signer = signer,
|
signer = signer,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+6
@@ -24,10 +24,13 @@ import com.vitorpamplona.amethyst.model.ALL_FOLLOWS
|
|||||||
import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS
|
import com.vitorpamplona.amethyst.model.ALL_USER_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.AROUND_ME
|
import com.vitorpamplona.amethyst.model.AROUND_ME
|
||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
|
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState
|
||||||
import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
|
import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
|
||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsFeedFlow
|
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsFeedFlow
|
||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsFeedFlow
|
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.AllUserFollowsFeedFlow
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.allUserFollows.Kind3UserFollowsFeedFlow
|
||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.AroundMeFeedFlow
|
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.AroundMeFeedFlow
|
||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalFeedFlow
|
import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalFeedFlow
|
||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.NoteFeedFlow
|
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.NoteFeedFlow
|
||||||
@@ -42,6 +45,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.emitAll
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -49,6 +53,7 @@ import kotlinx.coroutines.flow.transformLatest
|
|||||||
|
|
||||||
class FeedTopNavFilterState(
|
class FeedTopNavFilterState(
|
||||||
val feedFilterListName: MutableStateFlow<String>,
|
val feedFilterListName: MutableStateFlow<String>,
|
||||||
|
val kind3Follows: StateFlow<Kind3FollowListState.Kind3Follows>,
|
||||||
val allFollows: StateFlow<MergedFollowListsState.AllFollows>,
|
val allFollows: StateFlow<MergedFollowListsState.AllFollows>,
|
||||||
val locationFlow: StateFlow<LocationState.LocationResult>,
|
val locationFlow: StateFlow<LocationState.LocationResult>,
|
||||||
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||||
@@ -63,6 +68,7 @@ class FeedTopNavFilterState(
|
|||||||
GLOBAL_FOLLOWS -> GlobalFeedFlow(followsRelays, proxyRelays)
|
GLOBAL_FOLLOWS -> GlobalFeedFlow(followsRelays, proxyRelays)
|
||||||
ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays)
|
ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays)
|
||||||
ALL_USER_FOLLOWS -> AllUserFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays)
|
ALL_USER_FOLLOWS -> AllUserFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays)
|
||||||
|
KIND3_FOLLOWS -> Kind3UserFollowsFeedFlow(kind3Follows, followsRelays, blockedRelays, proxyRelays)
|
||||||
AROUND_ME -> AroundMeFeedFlow(locationFlow, followsRelays, proxyRelays)
|
AROUND_ME -> AroundMeFeedFlow(locationFlow, followsRelays, proxyRelays)
|
||||||
else -> {
|
else -> {
|
||||||
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* 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.model.topNavFeeds.allUserFollows
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
||||||
|
class Kind3UserFollowsFeedFlow(
|
||||||
|
val allFollows: StateFlow<Kind3FollowListState.Kind3Follows>,
|
||||||
|
val followsRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||||
|
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||||
|
val proxyRelays: StateFlow<Set<NormalizedRelayUrl>>,
|
||||||
|
) : IFeedFlowsType {
|
||||||
|
fun convert(
|
||||||
|
allFollows: Kind3FollowListState.Kind3Follows?,
|
||||||
|
proxyRelays: Set<NormalizedRelayUrl>,
|
||||||
|
): IFeedTopNavFilter =
|
||||||
|
if (allFollows != null) {
|
||||||
|
if (proxyRelays.isEmpty()) {
|
||||||
|
AllUserFollowsByOutboxTopNavFilter(
|
||||||
|
authors = allFollows.authors,
|
||||||
|
defaultRelays = followsRelays,
|
||||||
|
blockedRelays = blockedRelays,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AllUserFollowsByProxyTopNavFilter(
|
||||||
|
authors = allFollows.authors,
|
||||||
|
proxyRelays = proxyRelays,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AllUserFollowsByOutboxTopNavFilter(
|
||||||
|
authors = emptySet(),
|
||||||
|
defaultRelays = followsRelays,
|
||||||
|
blockedRelays = blockedRelays,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flow() = combine(allFollows, proxyRelays, ::convert)
|
||||||
|
|
||||||
|
override fun startValue(): IFeedTopNavFilter = convert(allFollows.value, proxyRelays.value)
|
||||||
|
|
||||||
|
override suspend fun startValue(collector: FlowCollector<IFeedTopNavFilter>) {
|
||||||
|
collector.emit(startValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-2
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.quartz.utils.asTextOrNull
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
@@ -127,7 +128,7 @@ class MeltProcessor {
|
|||||||
val msg =
|
val msg =
|
||||||
tree
|
tree
|
||||||
?.get("detail")
|
?.get("detail")
|
||||||
?.asText()
|
?.asTextOrNull()
|
||||||
?.split('.')
|
?.split('.')
|
||||||
?.getOrNull(0)
|
?.getOrNull(0)
|
||||||
?.ifBlank { null }
|
?.ifBlank { null }
|
||||||
@@ -203,7 +204,7 @@ class MeltProcessor {
|
|||||||
val msg =
|
val msg =
|
||||||
tree
|
tree
|
||||||
?.get("detail")
|
?.get("detail")
|
||||||
?.asText()
|
?.asTextOrNull()
|
||||||
?.split('.')
|
?.split('.')
|
||||||
?.getOrNull(0)
|
?.getOrNull(0)
|
||||||
?.ifBlank { null }
|
?.ifBlank { null }
|
||||||
|
|||||||
+38
-1
@@ -21,11 +21,14 @@
|
|||||||
package com.vitorpamplona.amethyst.service.relayClient
|
package com.vitorpamplona.amethyst.service.relayClient
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.EventCollector
|
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.EventCollector
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayInsertConfirmationCollector
|
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayInsertConfirmationCollector
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||||
|
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||||
|
|
||||||
class CacheClientConnector(
|
class CacheClientConnector(
|
||||||
val client: INostrClient,
|
val client: INostrClient,
|
||||||
@@ -50,5 +53,39 @@ class CacheClientConnector(
|
|||||||
private fun markAsSeen(
|
private fun markAsSeen(
|
||||||
eventId: HexKey,
|
eventId: HexKey,
|
||||||
info: NormalizedRelayUrl,
|
info: NormalizedRelayUrl,
|
||||||
) = LocalCache.getNoteIfExists(eventId)?.addRelay(info)
|
) {
|
||||||
|
val note = LocalCache.getNoteIfExists(eventId)
|
||||||
|
if (note != null) {
|
||||||
|
note.addRelay(info)
|
||||||
|
markAsSeenInner(note, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun markAsSeenInner(
|
||||||
|
note: Note,
|
||||||
|
info: NormalizedRelayUrl,
|
||||||
|
) {
|
||||||
|
val noteEvent = note.event
|
||||||
|
if (noteEvent is GiftWrapEvent) {
|
||||||
|
val innerEvent = noteEvent.innerEventId
|
||||||
|
if (innerEvent != null) {
|
||||||
|
val innerNote = cache.getNoteIfExists(innerEvent)
|
||||||
|
if (innerNote != null) {
|
||||||
|
innerNote.addRelay(info)
|
||||||
|
markAsSeenInner(innerNote, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noteEvent is SealedRumorEvent) {
|
||||||
|
val innerEvent = noteEvent.innerEventId
|
||||||
|
if (innerEvent != null) {
|
||||||
|
val innerNote = cache.getNoteIfExists(innerEvent)
|
||||||
|
if (innerNote != null) {
|
||||||
|
innerNote.addRelay(info)
|
||||||
|
markAsSeenInner(innerNote, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dataso
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssembler
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource.FollowPackFeedFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
||||||
@@ -77,6 +78,7 @@ class RelaySubscriptionsCoordinator(
|
|||||||
val profile = UserProfileFilterAssembler(client)
|
val profile = UserProfileFilterAssembler(client)
|
||||||
val hashtags = HashtagFilterAssembler(client)
|
val hashtags = HashtagFilterAssembler(client)
|
||||||
val geohashes = GeoHashFilterAssembler(client)
|
val geohashes = GeoHashFilterAssembler(client)
|
||||||
|
val followPacks = FollowPackFeedFilterAssembler(client)
|
||||||
|
|
||||||
// active when sending zaps via NWC
|
// active when sending zaps via NWC
|
||||||
val nwc = NWCPaymentFilterAssembler(client)
|
val nwc = NWCPaymentFilterAssembler(client)
|
||||||
|
|||||||
+27
-1
@@ -107,7 +107,7 @@ fun <T> observeNoteAndMap(
|
|||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@Composable
|
@Composable
|
||||||
fun <T, U> observeNoteEventAndMap(
|
fun <T, U> observeNoteEventAndMapNotNull(
|
||||||
note: Note,
|
note: Note,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
map: (T) -> U,
|
map: (T) -> U,
|
||||||
@@ -130,6 +130,32 @@ fun <T, U> observeNoteEventAndMap(
|
|||||||
return flow.collectAsStateWithLifecycle((note.event as? T)?.let { map(it) })
|
return flow.collectAsStateWithLifecycle((note.event as? T)?.let { map(it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@Composable
|
||||||
|
fun <T, U> observeNoteEventAndMap(
|
||||||
|
note: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
map: (T?) -> U,
|
||||||
|
): State<U> {
|
||||||
|
// Subscribe in the relay for changes in this note.
|
||||||
|
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||||
|
|
||||||
|
// Subscribe in the LocalCache for changes that arrive in the device
|
||||||
|
val flow =
|
||||||
|
remember(note) {
|
||||||
|
note
|
||||||
|
.flow()
|
||||||
|
.metadata.stateFlow
|
||||||
|
.mapLatest { map(it.note.event as? T) }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe in the LocalCache for changes that arrive in the device
|
||||||
|
return flow.collectAsStateWithLifecycle(map(note.event as? T))
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun observeNoteHasEvent(
|
fun observeNoteHasEvent(
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ fun EditPostView(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val postViewModel: EditPostViewModel = viewModel()
|
val postViewModel: EditPostViewModel = viewModel()
|
||||||
postViewModel.prepare(edit, versionLookingAt, accountViewModel)
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ fun EditPostView(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
postViewModel.load(edit, versionLookingAt, accountViewModel)
|
postViewModel.load(edit, versionLookingAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
|
|||||||
@@ -61,13 +61,12 @@ import com.vitorpamplona.quartz.nip94FileMetadata.originalHash
|
|||||||
import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent
|
import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.size
|
import com.vitorpamplona.quartz.nip94FileMetadata.size
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
open class EditPostViewModel : ViewModel() {
|
open class EditPostViewModel : ViewModel() {
|
||||||
var accountViewModel: AccountViewModel? = null
|
lateinit var accountViewModel: AccountViewModel
|
||||||
var account: Account? = null
|
lateinit var account: Account
|
||||||
|
|
||||||
var editedFromNote: Note? = null
|
var editedFromNote: Note? = null
|
||||||
|
|
||||||
@@ -94,46 +93,32 @@ open class EditPostViewModel : ViewModel() {
|
|||||||
var canAddInvoice by mutableStateOf(false)
|
var canAddInvoice by mutableStateOf(false)
|
||||||
var wantsInvoice by mutableStateOf(false)
|
var wantsInvoice by mutableStateOf(false)
|
||||||
|
|
||||||
open fun prepare(
|
open fun init(accountViewModel: AccountViewModel) {
|
||||||
edit: Note,
|
|
||||||
versionLookingAt: Note?,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
this.accountViewModel = accountViewModel
|
this.accountViewModel = accountViewModel
|
||||||
this.account = accountViewModel.account
|
this.account = accountViewModel.account
|
||||||
this.editedFromNote = edit
|
|
||||||
|
|
||||||
this.userSuggestions?.reset()
|
|
||||||
this.userSuggestions = UserSuggestionState(accountViewModel.account)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun load(
|
open fun load(
|
||||||
edit: Note,
|
edit: Note,
|
||||||
versionLookingAt: Note?,
|
versionLookingAt: Note?,
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
) {
|
||||||
this.accountViewModel = accountViewModel
|
|
||||||
this.account = accountViewModel.account
|
|
||||||
|
|
||||||
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
|
|
||||||
message = TextFieldValue(versionLookingAt?.event?.content ?: edit.event?.content ?: "")
|
message = TextFieldValue(versionLookingAt?.event?.content ?: edit.event?.content ?: "")
|
||||||
urlPreview = findUrlInMessage()
|
urlPreview = findUrlInMessage()
|
||||||
|
|
||||||
editedFromNote = edit
|
this.editedFromNote = edit
|
||||||
|
|
||||||
|
this.userSuggestions?.reset()
|
||||||
|
this.userSuggestions = UserSuggestionState(accountViewModel.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendPost() {
|
fun sendPost() {
|
||||||
viewModelScope.launch(Dispatchers.IO) { innerSendPost() }
|
accountViewModel.launchSigner(::innerSendPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun innerSendPost() {
|
suspend fun innerSendPost() {
|
||||||
if (accountViewModel == null) {
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val extraNotesToBroadcast = mutableListOf<Event>()
|
val extraNotesToBroadcast = mutableListOf<Event>()
|
||||||
|
|
||||||
nip95attachments.forEach {
|
nip95attachments.forEach {
|
||||||
@@ -142,14 +127,14 @@ open class EditPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val notify =
|
val notify =
|
||||||
if (editedFromNote?.author?.pubkeyHex == account?.userProfile()?.pubkeyHex) {
|
if (editedFromNote?.author?.pubkeyHex == account.userProfile().pubkeyHex) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
// notifies if it is not the logged in user
|
// notifies if it is not the logged in user
|
||||||
editedFromNote?.author?.pubkeyHex
|
editedFromNote?.author?.pubkeyHex
|
||||||
}
|
}
|
||||||
|
|
||||||
account?.sendEdit(
|
account.sendEdit(
|
||||||
message = message.text,
|
message = message.text,
|
||||||
originalNote = editedFromNote!!,
|
originalNote = editedFromNote!!,
|
||||||
notify = notify,
|
notify = notify,
|
||||||
@@ -191,7 +176,7 @@ open class EditPostViewModel : ViewModel() {
|
|||||||
context: Context,
|
context: Context,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val myAccount = account ?: return@launch
|
val myAccount = account
|
||||||
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
||||||
|
|
||||||
isUploadingImage = true
|
isUploadingImage = true
|
||||||
@@ -218,7 +203,7 @@ open class EditPostViewModel : ViewModel() {
|
|||||||
contentWarningReason = if (sensitiveContent) "" else null,
|
contentWarningReason = if (sensitiveContent) "" else null,
|
||||||
)
|
)
|
||||||
nip95attachments = nip95attachments + nip95
|
nip95attachments = nip95attachments + nip95
|
||||||
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
|
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||||
|
|
||||||
note?.let {
|
note?.let {
|
||||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||||
|
|||||||
+3
-3
@@ -102,7 +102,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun create() {
|
fun create() {
|
||||||
// Tries to not delete any existing attribute that we do not work with.
|
// Tries to not delete any existing attribute that we do not work with.
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
val metadata =
|
val metadata =
|
||||||
account.userMetadata.sendNewUserMetadata(
|
account.userMetadata.sendNewUserMetadata(
|
||||||
name = displayName.value,
|
name = displayName.value,
|
||||||
@@ -145,7 +145,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
context: Context,
|
context: Context,
|
||||||
onError: (String, String) -> Unit,
|
onError: (String, String) -> Unit,
|
||||||
) {
|
) {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
upload(
|
upload(
|
||||||
uri,
|
uri,
|
||||||
context,
|
context,
|
||||||
@@ -161,7 +161,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
context: Context,
|
context: Context,
|
||||||
onError: (String, String) -> Unit,
|
onError: (String, String) -> Unit,
|
||||||
) {
|
) {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
upload(
|
upload(
|
||||||
uri,
|
uri,
|
||||||
context,
|
context,
|
||||||
|
|||||||
+6
-3
@@ -52,9 +52,12 @@ fun AllMediaServersScreen(
|
|||||||
val nip96ServersViewModel: NIP96ServersViewModel = viewModel()
|
val nip96ServersViewModel: NIP96ServersViewModel = viewModel()
|
||||||
val blossomServersViewModel: BlossomServersViewModel = viewModel()
|
val blossomServersViewModel: BlossomServersViewModel = viewModel()
|
||||||
|
|
||||||
LaunchedEffect(key1 = Unit) {
|
nip96ServersViewModel.init(accountViewModel)
|
||||||
nip96ServersViewModel.load(accountViewModel.account)
|
blossomServersViewModel.init(accountViewModel)
|
||||||
blossomServersViewModel.load(accountViewModel.account)
|
|
||||||
|
LaunchedEffect(key1 = accountViewModel) {
|
||||||
|
nip96ServersViewModel.load()
|
||||||
|
blossomServersViewModel.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaServersScaffold(nip96ServersViewModel, blossomServersViewModel) {
|
MediaServersScaffold(nip96ServersViewModel, blossomServersViewModel) {
|
||||||
|
|||||||
+12
-7
@@ -23,23 +23,28 @@ package com.vitorpamplona.amethyst.ui.actions.mediaServers
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import com.vitorpamplona.quartz.utils.Rfc3986
|
import com.vitorpamplona.quartz.utils.Rfc3986
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class BlossomServersViewModel : ViewModel() {
|
class BlossomServersViewModel : ViewModel() {
|
||||||
lateinit var account: Account
|
private lateinit var accountViewModel: AccountViewModel
|
||||||
|
private lateinit var account: Account
|
||||||
|
|
||||||
private val _fileServers = MutableStateFlow<List<ServerName>>(emptyList())
|
private val _fileServers = MutableStateFlow<List<ServerName>>(emptyList())
|
||||||
val fileServers = _fileServers.asStateFlow()
|
val fileServers = _fileServers.asStateFlow()
|
||||||
private var isModified = false
|
private var isModified = false
|
||||||
|
|
||||||
fun load(account: Account) {
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
this.account = account
|
this.accountViewModel = accountViewModel
|
||||||
|
this.account = accountViewModel.account
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load() {
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +107,7 @@ class BlossomServersViewModel : ViewModel() {
|
|||||||
serverUrl: String,
|
serverUrl: String,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val serverName = if (name.isNotBlank()) name else Rfc3986.host(serverUrl)
|
val serverName = name.ifBlank { Rfc3986.host(serverUrl) }
|
||||||
_fileServers.update {
|
_fileServers.update {
|
||||||
it.minus(
|
it.minus(
|
||||||
ServerName(serverName, serverUrl, ServerType.Blossom),
|
ServerName(serverName, serverUrl, ServerType.Blossom),
|
||||||
@@ -119,7 +124,7 @@ class BlossomServersViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun saveFileServers() {
|
fun saveFileServers() {
|
||||||
if (isModified) {
|
if (isModified) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
accountViewModel.launchSigner {
|
||||||
val serverList = _fileServers.value.map { it.baseUrl }
|
val serverList = _fileServers.value.map { it.baseUrl }
|
||||||
account.sendBlossomServersList(serverList)
|
account.sendBlossomServersList(serverList)
|
||||||
refresh()
|
refresh()
|
||||||
@@ -127,5 +132,5 @@ class BlossomServersViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun obtainFileServers(): List<String>? = account.blossomServers.flow.value
|
private fun obtainFileServers(): List<String> = account.blossomServers.flow.value
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -23,23 +23,28 @@ package com.vitorpamplona.amethyst.ui.actions.mediaServers
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import com.vitorpamplona.quartz.utils.Rfc3986
|
import com.vitorpamplona.quartz.utils.Rfc3986
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class NIP96ServersViewModel : ViewModel() {
|
class NIP96ServersViewModel : ViewModel() {
|
||||||
lateinit var account: Account
|
private lateinit var accountViewModel: AccountViewModel
|
||||||
|
private lateinit var account: Account
|
||||||
|
|
||||||
private val _fileServers = MutableStateFlow<List<ServerName>>(emptyList())
|
private val _fileServers = MutableStateFlow<List<ServerName>>(emptyList())
|
||||||
val fileServers = _fileServers.asStateFlow()
|
val fileServers = _fileServers.asStateFlow()
|
||||||
private var isModified = false
|
private var isModified = false
|
||||||
|
|
||||||
fun load(account: Account) {
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
this.account = account
|
this.accountViewModel = accountViewModel
|
||||||
|
this.account = accountViewModel.account
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load() {
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@ class NIP96ServersViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun saveFileServers() {
|
fun saveFileServers() {
|
||||||
if (isModified) {
|
if (isModified) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
accountViewModel.launchSigner {
|
||||||
val serverList = _fileServers.value.map { it.baseUrl }
|
val serverList = _fileServers.value.map { it.baseUrl }
|
||||||
account.sendFileServersList(serverList)
|
account.sendFileServersList(serverList)
|
||||||
refresh()
|
refresh()
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ fun LoadOrCreateNote(
|
|||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
LaunchedEffect(key1 = event.id) {
|
LaunchedEffect(key1 = event.id) {
|
||||||
accountViewModel.checkGetOrCreateNote(event) { note = it }
|
note = accountViewModel.noteFromEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ fun DisplayUser(
|
|||||||
|
|
||||||
if (userBase == null) {
|
if (userBase == null) {
|
||||||
LaunchedEffect(key1 = userHex) {
|
LaunchedEffect(key1 = userHex) {
|
||||||
accountViewModel.checkGetOrCreateUser(userHex) { userBase = it }
|
userBase = accountViewModel.checkGetOrCreateUser(userHex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,11 +192,11 @@ fun RenderRegularPreview() {
|
|||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
route = Route.EventRedirect(word.segmentText),
|
route = Route.EventRedirect(word.segmentText),
|
||||||
nav = EmptyNav,
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HashTagSegment -> HashTag(word, EmptyNav)
|
is HashTagSegment -> HashTag(word, EmptyNav())
|
||||||
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||||
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
||||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||||
@@ -225,7 +225,7 @@ fun RenderRegularPreview2() {
|
|||||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||||
// is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
|
// is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
|
||||||
is HashTagSegment -> HashTag(word, EmptyNav)
|
is HashTagSegment -> HashTag(word, EmptyNav())
|
||||||
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||||
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
||||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||||
@@ -267,7 +267,7 @@ fun RenderRegularPreview3() {
|
|||||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||||
// is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
|
// is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
|
||||||
is HashTagSegment -> HashTag(word, EmptyNav)
|
is HashTagSegment -> HashTag(word, EmptyNav())
|
||||||
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||||
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
||||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||||
@@ -746,7 +746,7 @@ fun LoadNote(
|
|||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
LaunchedEffect(key1 = baseNoteHex) {
|
LaunchedEffect(key1 = baseNoteHex) {
|
||||||
accountViewModel.checkGetOrCreateNote(baseNoteHex) { note = it }
|
note = accountViewModel.checkGetOrCreateNote(baseNoteHex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -122,7 +122,7 @@ fun RenderContentAsMarkdown(
|
|||||||
fun RenderContentAsMarkdownPreview() {
|
fun RenderContentAsMarkdownPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
ThemeComparisonRow {
|
ThemeComparisonRow {
|
||||||
val background = MaterialTheme.colorScheme.background
|
val background = MaterialTheme.colorScheme.background
|
||||||
@@ -174,7 +174,7 @@ fun RenderContentAsMarkdownPreview() {
|
|||||||
fun RenderContentAsMarkdownListsPreview() {
|
fun RenderContentAsMarkdownListsPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
ThemeComparisonRow {
|
ThemeComparisonRow {
|
||||||
val background = MaterialTheme.colorScheme.background
|
val background = MaterialTheme.colorScheme.background
|
||||||
@@ -224,7 +224,7 @@ fun RenderContentAsMarkdownListsPreview() {
|
|||||||
fun RenderContentAsMarkdownCodePreview() {
|
fun RenderContentAsMarkdownCodePreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
ThemeComparisonRow {
|
ThemeComparisonRow {
|
||||||
val background = MaterialTheme.colorScheme.background
|
val background = MaterialTheme.colorScheme.background
|
||||||
@@ -278,7 +278,7 @@ fun RenderContentAsMarkdownCodePreview() {
|
|||||||
fun RenderContentAsMarkdownTablesPreview() {
|
fun RenderContentAsMarkdownTablesPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
ThemeComparisonRow {
|
ThemeComparisonRow {
|
||||||
val background = MaterialTheme.colorScheme.background
|
val background = MaterialTheme.colorScheme.background
|
||||||
@@ -319,7 +319,7 @@ fun RenderContentAsMarkdownTablesPreview() {
|
|||||||
fun RenderContentAsMarkdownFootNotesPreview() {
|
fun RenderContentAsMarkdownFootNotesPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
ThemeComparisonRow {
|
ThemeComparisonRow {
|
||||||
val background = MaterialTheme.colorScheme.background
|
val background = MaterialTheme.colorScheme.background
|
||||||
@@ -356,7 +356,7 @@ fun RenderContentAsMarkdownFootNotesPreview() {
|
|||||||
fun RenderContentAsMarkdownUserPreview() {
|
fun RenderContentAsMarkdownUserPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -418,7 +418,7 @@ fun RenderContentAsMarkdownUserPreview() {
|
|||||||
fun RenderContentAsMarkdownNotePreview() {
|
fun RenderContentAsMarkdownNotePreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|||||||
+1
-1
@@ -90,7 +90,7 @@ fun ErrorListPreview() {
|
|||||||
ErrorList(
|
ErrorList(
|
||||||
model = model,
|
model = model,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = EmptyNav,
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ import kotlinx.coroutines.withContext
|
|||||||
@Preview
|
@Preview
|
||||||
fun MultiUserErrorMessageContentPreview() {
|
fun MultiUserErrorMessageContentPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
var user1: User? = null
|
var user1: User? = null
|
||||||
var user2: User? = null
|
var user2: User? = null
|
||||||
|
|||||||
@@ -73,15 +73,19 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPackFeedScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists.PeopleListScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.packs.FollowPackScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.ListOfPeopleListsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.ListOfPeopleListsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit.EditPeopleListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.FollowPackMetadataScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
||||||
@@ -127,8 +131,12 @@ fun AppNavigation(
|
|||||||
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
|
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromEnd<Route.Lists> { ListOfPeopleListsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.Lists> { ListOfPeopleListsScreen(accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.PeopleListView> { PeopleListScreen(it.dTag, accountViewModel, nav) }
|
composableFromEndArgs<Route.MyPeopleListView> { PeopleListScreen(it.dTag, accountViewModel, nav) }
|
||||||
composableFromBottomArgs<Route.PeopleListManagement> { EditPeopleListScreen(it.userToAdd, accountViewModel, nav) }
|
composableFromEndArgs<Route.MyFollowPackView> { FollowPackScreen(it.dTag, accountViewModel, nav) }
|
||||||
|
composableFromBottomArgs<Route.PeopleListManagement> { FollowListAndPackAndUserScreen(it.userToAdd, accountViewModel, nav) }
|
||||||
|
|
||||||
|
composableFromBottomArgs<Route.PeopleListMetadataEdit> { PeopleListMetadataScreen(it.dTag, accountViewModel, nav) }
|
||||||
|
composableFromBottomArgs<Route.FollowPackMetadataEdit> { FollowPackMetadataScreen(it.dTag, accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromEnd<Route.BookmarkGroups> { ListOfBookmarkGroupsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.BookmarkGroups> { ListOfBookmarkGroupsScreen(accountViewModel, nav) }
|
||||||
|
|
||||||
@@ -152,6 +160,7 @@ fun AppNavigation(
|
|||||||
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
|
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
composableFromEndArgs<Route.Community> { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||||
|
composableFromEndArgs<Route.FollowPack> { FollowPackFeedScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, it.expiresDays, accountViewModel, nav) }
|
composableFromEndArgs<Route.Room> { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, it.expiresDays, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
|
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
|
||||||
|
|||||||
+2
-36
@@ -525,7 +525,7 @@ fun ListContent(
|
|||||||
|
|
||||||
NavigationRow(
|
NavigationRow(
|
||||||
title = R.string.user_preferences,
|
title = R.string.user_preferences,
|
||||||
icons = listOf(Icons.Outlined.Translate),
|
icon = Icons.Outlined.Translate,
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
route = Route.UserSettings,
|
route = Route.UserSettings,
|
||||||
@@ -608,27 +608,10 @@ fun NavigationRow(
|
|||||||
tint: Color,
|
tint: Color,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
route: Route,
|
route: Route,
|
||||||
) {
|
|
||||||
NavigationRow(
|
|
||||||
title = title,
|
|
||||||
icons = listOf(icon),
|
|
||||||
tint = tint,
|
|
||||||
nav = nav,
|
|
||||||
route = route,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NavigationRow(
|
|
||||||
title: Int,
|
|
||||||
icons: List<ImageVector>,
|
|
||||||
tint: Color,
|
|
||||||
nav: INav,
|
|
||||||
route: Route,
|
|
||||||
) {
|
) {
|
||||||
IconRow(
|
IconRow(
|
||||||
title = title,
|
title = title,
|
||||||
icons = icons,
|
icon = icon,
|
||||||
tint = tint,
|
tint = tint,
|
||||||
onClick = {
|
onClick = {
|
||||||
nav.closeDrawer()
|
nav.closeDrawer()
|
||||||
@@ -678,21 +661,6 @@ fun IconRow(
|
|||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
tint: Color,
|
tint: Color,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
|
||||||
IconRow(
|
|
||||||
title = title,
|
|
||||||
icons = listOf(icon),
|
|
||||||
tint = tint,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun IconRow(
|
|
||||||
title: Int,
|
|
||||||
icons: List<ImageVector>,
|
|
||||||
tint: Color,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -707,14 +675,12 @@ fun IconRow(
|
|||||||
modifier = IconRowModifier,
|
modifier = IconRowModifier,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
icons.forEach { icon ->
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = stringRes(title),
|
contentDescription = stringRes(title),
|
||||||
modifier = Size22Modifier.padding(end = 4.dp),
|
modifier = Size22Modifier.padding(end = 4.dp),
|
||||||
tint = tint,
|
tint = tint,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = IconRowTextModifier,
|
modifier = IconRowTextModifier,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
object EmptyNav : INav {
|
class EmptyNav : INav {
|
||||||
override val navigationScope: CoroutineScope get() = TODO("Not yet implemented")
|
override val navigationScope: CoroutineScope get() = TODO("Not yet implemented")
|
||||||
override val drawerState = DrawerState(DrawerValue.Closed)
|
override val drawerState = DrawerState(DrawerValue.Closed)
|
||||||
|
|
||||||
|
|||||||
+71
-59
@@ -43,6 +43,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
|||||||
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||||
@@ -65,73 +66,82 @@ fun routeFor(
|
|||||||
fun routeFor(
|
fun routeFor(
|
||||||
noteEvent: Event,
|
noteEvent: Event,
|
||||||
loggedIn: Account,
|
loggedIn: Account,
|
||||||
): Route? {
|
): Route? =
|
||||||
if (noteEvent is DraftWrapEvent) {
|
if (noteEvent is DraftWrapEvent) {
|
||||||
val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent)
|
val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent)
|
||||||
|
|
||||||
if (innerEvent is IsInPublicChatChannel) {
|
if (innerEvent != null) {
|
||||||
innerEvent.channelId()?.let {
|
routeForInner(innerEvent, loggedIn)
|
||||||
return Route.PublicChatChannel(it)
|
|
||||||
}
|
|
||||||
} else if (innerEvent is LiveActivitiesEvent) {
|
|
||||||
innerEvent.address().let {
|
|
||||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
|
||||||
}
|
|
||||||
} else if (innerEvent is LiveActivitiesChatMessageEvent) {
|
|
||||||
innerEvent.activityAddress()?.let {
|
|
||||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
|
||||||
}
|
|
||||||
} else if (innerEvent is EphemeralChatEvent) {
|
|
||||||
innerEvent.roomId()?.let {
|
|
||||||
return Route.EphemeralChat(it.id, it.relayUrl.url)
|
|
||||||
}
|
|
||||||
} else if (innerEvent is ChatroomKeyable) {
|
|
||||||
val room = innerEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
|
|
||||||
loggedIn.chatroomList.getOrCreatePrivateChatroom(room)
|
|
||||||
return Route.Room(room)
|
|
||||||
} else if (innerEvent is AddressableEvent) {
|
|
||||||
return Route.Note(noteEvent.aTag().toTag())
|
|
||||||
} else {
|
} else {
|
||||||
return Route.Note(noteEvent.id)
|
Route.Note(noteEvent.id)
|
||||||
}
|
}
|
||||||
} else if (noteEvent is AppDefinitionEvent) {
|
|
||||||
return Route.ContentDiscovery(noteEvent.id)
|
|
||||||
} else if (noteEvent is IsInPublicChatChannel) {
|
|
||||||
noteEvent.channelId()?.let {
|
|
||||||
return Route.PublicChatChannel(it)
|
|
||||||
}
|
|
||||||
} else if (noteEvent is ChannelCreateEvent) {
|
|
||||||
return Route.PublicChatChannel(noteEvent.id)
|
|
||||||
} else if (noteEvent is LiveActivitiesEvent) {
|
|
||||||
noteEvent.address().let {
|
|
||||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
|
||||||
}
|
|
||||||
} else if (noteEvent is LiveActivitiesChatMessageEvent) {
|
|
||||||
noteEvent.activityAddress()?.let {
|
|
||||||
return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
|
||||||
}
|
|
||||||
} else if (noteEvent is ChatroomKeyable) {
|
|
||||||
val room = noteEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
|
|
||||||
loggedIn.chatroomList.getOrCreatePrivateChatroom(room)
|
|
||||||
return Route.Room(room)
|
|
||||||
} else if (noteEvent is CommunityDefinitionEvent) {
|
|
||||||
return Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag())
|
|
||||||
} else if (noteEvent is GiftWrapEvent) {
|
|
||||||
noteEvent.innerEventId?.let {
|
|
||||||
return routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
|
||||||
}
|
|
||||||
} else if (noteEvent is SealedRumorEvent) {
|
|
||||||
noteEvent.innerEventId?.let {
|
|
||||||
return routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
|
||||||
}
|
|
||||||
} else if (noteEvent is AddressableEvent) {
|
|
||||||
return Route.Note(noteEvent.aTag().toTag())
|
|
||||||
} else {
|
} else {
|
||||||
return Route.Note(noteEvent.id)
|
routeForInner(noteEvent, loggedIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
fun routeForInner(
|
||||||
}
|
noteEvent: Event,
|
||||||
|
loggedIn: Account,
|
||||||
|
): Route? =
|
||||||
|
when (noteEvent) {
|
||||||
|
is AppDefinitionEvent -> {
|
||||||
|
Route.ContentDiscovery(noteEvent.id)
|
||||||
|
}
|
||||||
|
is IsInPublicChatChannel -> {
|
||||||
|
noteEvent.channelId()?.let {
|
||||||
|
Route.PublicChatChannel(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ChannelCreateEvent -> {
|
||||||
|
Route.PublicChatChannel(noteEvent.id)
|
||||||
|
}
|
||||||
|
is LiveActivitiesEvent -> {
|
||||||
|
noteEvent.address().let {
|
||||||
|
Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LiveActivitiesChatMessageEvent -> {
|
||||||
|
noteEvent.activityAddress()?.let {
|
||||||
|
Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is EphemeralChatEvent -> {
|
||||||
|
noteEvent.roomId()?.let {
|
||||||
|
Route.EphemeralChat(it.id, it.relayUrl.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is FollowListEvent -> {
|
||||||
|
Route.FollowPack(noteEvent.address())
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChatroomKeyable -> {
|
||||||
|
val room = noteEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
|
||||||
|
loggedIn.chatroomList.getOrCreatePrivateChatroom(room)
|
||||||
|
Route.Room(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
is CommunityDefinitionEvent -> {
|
||||||
|
Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag())
|
||||||
|
}
|
||||||
|
is GiftWrapEvent -> {
|
||||||
|
noteEvent.innerEventId?.let {
|
||||||
|
routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is SealedRumorEvent -> {
|
||||||
|
noteEvent.innerEventId?.let {
|
||||||
|
routeFor(LocalCache.getOrCreateNote(it), loggedIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is AddressableEvent -> {
|
||||||
|
Route.Note(noteEvent.aTag().toTag())
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Route.Note(noteEvent.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun routeToMessage(
|
fun routeToMessage(
|
||||||
user: HexKey,
|
user: HexKey,
|
||||||
@@ -207,6 +217,8 @@ fun routeFor(roomId: RoomId): Route = Route.EphemeralChat(roomId.id, roomId.rela
|
|||||||
|
|
||||||
fun routeFor(user: User): Route.Profile = Route.Profile(user.pubkeyHex)
|
fun routeFor(user: User): Route.Profile = Route.Profile(user.pubkeyHex)
|
||||||
|
|
||||||
|
fun routeForUser(userHex: HexKey): Route.Profile = Route.Profile(userHex)
|
||||||
|
|
||||||
fun authorRouteFor(note: Note): Route.Profile? = note.author?.pubkeyHex?.let { Route.Profile(it) }
|
fun authorRouteFor(note: Note): Route.Profile? = note.author?.pubkeyHex?.let { Route.Profile(it) }
|
||||||
|
|
||||||
fun routeReplyTo(
|
fun routeReplyTo(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.navigation.routes
|
|||||||
import androidx.navigation.NavDestination.Companion.hasRoute
|
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -56,10 +57,22 @@ sealed class Route {
|
|||||||
|
|
||||||
@Serializable object Lists : Route()
|
@Serializable object Lists : Route()
|
||||||
|
|
||||||
@Serializable data class PeopleListView(
|
@Serializable data class MyPeopleListView(
|
||||||
val dTag: String,
|
val dTag: String,
|
||||||
) : Route()
|
) : Route()
|
||||||
|
|
||||||
|
@Serializable data class MyFollowPackView(
|
||||||
|
val dTag: String,
|
||||||
|
) : Route()
|
||||||
|
|
||||||
|
@Serializable data class PeopleListMetadataEdit(
|
||||||
|
val dTag: String? = null,
|
||||||
|
) : Route()
|
||||||
|
|
||||||
|
@Serializable data class FollowPackMetadataEdit(
|
||||||
|
val dTag: String? = null,
|
||||||
|
) : Route()
|
||||||
|
|
||||||
@Serializable data class PeopleListManagement(
|
@Serializable data class PeopleListManagement(
|
||||||
val userToAdd: HexKey,
|
val userToAdd: HexKey,
|
||||||
) : Route()
|
) : Route()
|
||||||
@@ -98,7 +111,25 @@ sealed class Route {
|
|||||||
val kind: Int,
|
val kind: Int,
|
||||||
val pubKeyHex: HexKey,
|
val pubKeyHex: HexKey,
|
||||||
val dTag: String,
|
val dTag: String,
|
||||||
) : Route()
|
) : Route() {
|
||||||
|
constructor(address: Address) : this(
|
||||||
|
kind = address.kind,
|
||||||
|
pubKeyHex = address.pubKeyHex,
|
||||||
|
dTag = address.dTag,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable data class FollowPack(
|
||||||
|
val kind: Int,
|
||||||
|
val pubKeyHex: HexKey,
|
||||||
|
val dTag: String,
|
||||||
|
) : Route() {
|
||||||
|
constructor(address: Address) : this(
|
||||||
|
kind = address.kind,
|
||||||
|
pubKeyHex = address.pubKeyHex,
|
||||||
|
dTag = address.dTag,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable data class PublicChatChannel(
|
@Serializable data class PublicChatChannel(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -273,6 +304,15 @@ fun getRouteWithArguments(navController: NavHostController): Route? {
|
|||||||
dest.hasRoute<Route.GenericCommentPost>() -> entry.toRoute<Route.GenericCommentPost>()
|
dest.hasRoute<Route.GenericCommentPost>() -> entry.toRoute<Route.GenericCommentPost>()
|
||||||
dest.hasRoute<Route.NewPublicMessage>() -> entry.toRoute<Route.NewPublicMessage>()
|
dest.hasRoute<Route.NewPublicMessage>() -> entry.toRoute<Route.NewPublicMessage>()
|
||||||
|
|
||||||
|
dest.hasRoute<Route.Lists>() -> entry.toRoute<Route.Lists>()
|
||||||
|
dest.hasRoute<Route.MyPeopleListView>() -> entry.toRoute<Route.MyPeopleListView>()
|
||||||
|
dest.hasRoute<Route.MyFollowPackView>() -> entry.toRoute<Route.MyFollowPackView>()
|
||||||
|
dest.hasRoute<Route.PeopleListMetadataEdit>() -> entry.toRoute<Route.PeopleListMetadataEdit>()
|
||||||
|
dest.hasRoute<Route.FollowPackMetadataEdit>() -> entry.toRoute<Route.FollowPackMetadataEdit>()
|
||||||
|
dest.hasRoute<Route.PeopleListManagement>() -> entry.toRoute<Route.PeopleListManagement>()
|
||||||
|
dest.hasRoute<Route.NewGroupDM>() -> entry.toRoute<Route.NewGroupDM>()
|
||||||
|
dest.hasRoute<Route.UserSettings>() -> entry.toRoute<Route.UserSettings>()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,16 +95,13 @@ fun BlankNote(
|
|||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun HiddenNotePreview() {
|
fun HiddenNotePreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
|
||||||
val nav = EmptyNav
|
|
||||||
|
|
||||||
ThemeComparisonColumn(
|
ThemeComparisonColumn(
|
||||||
toPreview = {
|
toPreview = {
|
||||||
HiddenNote(
|
HiddenNote(
|
||||||
reports = persistentSetOf<Note>(),
|
reports = persistentSetOf<Note>(),
|
||||||
isHiddenAuthor = true,
|
isHiddenAuthor = true,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = mockAccountViewModel(),
|
||||||
nav = nav,
|
nav = EmptyNav(),
|
||||||
) {}
|
) {}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ import androidx.compose.ui.unit.sp
|
|||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeForUser
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@@ -81,3 +83,45 @@ fun Gallery(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun GalleryUnloaded(
|
||||||
|
users: ImmutableList<HexKey>,
|
||||||
|
modifier: Modifier,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
maxPictures: Int = 6,
|
||||||
|
) {
|
||||||
|
FlowRow(
|
||||||
|
modifier,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy((-5).dp),
|
||||||
|
) {
|
||||||
|
users.take(maxPictures).forEach {
|
||||||
|
ClickableUserPicture(
|
||||||
|
it,
|
||||||
|
Size25dp,
|
||||||
|
accountViewModel,
|
||||||
|
onClick = {
|
||||||
|
nav.nav {
|
||||||
|
routeForUser(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (users.size > maxPictures) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.size(Size25dp).clip(shape = CircleShape).background(MaterialTheme.colorScheme.secondaryContainer),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "+" + showCount(users.size - maxPictures),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -708,7 +708,7 @@ private fun RenderNoteRow(
|
|||||||
is BadgeAwardEvent -> RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
|
is BadgeAwardEvent -> RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
is FhirResourceEvent -> RenderFhirResource(baseNote, accountViewModel, nav)
|
is FhirResourceEvent -> RenderFhirResource(baseNote, accountViewModel, nav)
|
||||||
is PeopleListEvent -> DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
is PeopleListEvent -> DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
is FollowListEvent -> DisplayFollowList(baseNote, backgroundColor, accountViewModel, nav)
|
is FollowListEvent -> DisplayFollowList(baseNote, true, accountViewModel, nav)
|
||||||
is RelaySetEvent -> DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
|
is RelaySetEvent -> DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
is ChatMessageRelayListEvent -> DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
is ChatMessageRelayListEvent -> DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
is AdvertisedRelayListEvent -> DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
|
is AdvertisedRelayListEvent -> DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ fun PollNotePreview() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val accountViewModel = mockVitorAccountViewModel()
|
val accountViewModel = mockVitorAccountViewModel()
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
val baseNote: Note?
|
val baseNote: Note?
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
@@ -205,7 +205,7 @@ fun PollNotePreview2() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
val baseNote: Note?
|
val baseNote: Note?
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size17Modifier
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.noteComposeRelayBox
|
import com.vitorpamplona.amethyst.ui.theme.noteComposeRelayBox
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RelayBadges(
|
fun RelayBadges(
|
||||||
@@ -122,12 +123,14 @@ fun WatchAndRenderRelay(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val noteRelays by baseNote
|
val relay by baseNote
|
||||||
.flow()
|
.flow()
|
||||||
.relays.stateFlow
|
.relays.stateFlow
|
||||||
.collectAsStateWithLifecycle()
|
.mapNotNull {
|
||||||
|
it.note.relays.getOrNull(relayIndex)
|
||||||
|
}.collectAsStateWithLifecycle(baseNote.relays.getOrNull(relayIndex))
|
||||||
|
|
||||||
CrossfadeIfEnabled(targetState = noteRelays.note.relays.getOrNull(relayIndex), label = "RenderRelay", modifier = Size17Modifier, accountViewModel = accountViewModel) {
|
CrossfadeIfEnabled(targetState = relay, label = "RenderRelay", modifier = Size17Modifier, accountViewModel = accountViewModel) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
RenderRelay(it, accountViewModel, nav)
|
RenderRelay(it, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -77,7 +77,7 @@ import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.service.firstFullChar
|
import com.vitorpamplona.amethyst.service.firstFullChar
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull
|
||||||
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius
|
||||||
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||||
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
||||||
@@ -370,7 +370,7 @@ private fun EmojiSelector(
|
|||||||
accountViewModel,
|
accountViewModel,
|
||||||
) { emptyNote ->
|
) { emptyNote ->
|
||||||
emptyNote?.let { usersEmojiList ->
|
emptyNote?.let { usersEmojiList ->
|
||||||
val collections by observeNoteEventAndMap(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent ->
|
val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent ->
|
||||||
event.emojiPacks().toImmutableList()
|
event.emojiPacks().toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -175,6 +176,40 @@ fun ClickableUserPicture(
|
|||||||
BaseUserPicture(baseUser, size, accountViewModel, modifier, myModifier)
|
BaseUserPicture(baseUser, size, accountViewModel, modifier, myModifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ClickableUserPicture(
|
||||||
|
baseUserHex: HexKey,
|
||||||
|
size: Dp,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: ((HexKey) -> Unit)? = null,
|
||||||
|
onLongClick: ((HexKey) -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
// BaseUser is the same reference as accountState.user
|
||||||
|
val myModifier =
|
||||||
|
remember(baseUserHex) {
|
||||||
|
if (onClick != null && onLongClick != null) {
|
||||||
|
Modifier
|
||||||
|
.size(size)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onClick(baseUserHex) },
|
||||||
|
onLongClick = { onLongClick(baseUserHex) },
|
||||||
|
)
|
||||||
|
} else if (onClick != null) {
|
||||||
|
Modifier
|
||||||
|
.size(size)
|
||||||
|
.clickable(
|
||||||
|
onClick = { onClick(baseUserHex) },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier.size(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseUserPicture(baseUserHex, size, accountViewModel, modifier, myModifier)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NonClickableUserPictures(
|
fun NonClickableUserPictures(
|
||||||
room: ChatroomKey,
|
room: ChatroomKey,
|
||||||
@@ -322,6 +357,34 @@ fun BaseUserPicture(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BaseUserPicture(
|
||||||
|
baseUserHex: HexKey,
|
||||||
|
size: Dp,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
innerModifier: Modifier = Modifier,
|
||||||
|
outerModifier: Modifier = Modifier.size(size),
|
||||||
|
) {
|
||||||
|
Box(outerModifier, contentAlignment = Alignment.TopEnd) {
|
||||||
|
LoadUserProfilePicture(baseUserHex, accountViewModel) { userProfilePicture, userName ->
|
||||||
|
InnerUserPicture(
|
||||||
|
userHex = baseUserHex,
|
||||||
|
userPicture = userProfilePicture,
|
||||||
|
userName = userName,
|
||||||
|
size = size,
|
||||||
|
modifier = innerModifier,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchUserFollows(baseUserHex, accountViewModel) { newFollowingState ->
|
||||||
|
if (newFollowingState) {
|
||||||
|
FollowingIcon(Modifier.size(size.div(3.5f)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadUserProfilePicture(
|
fun LoadUserProfilePicture(
|
||||||
baseUser: User,
|
baseUser: User,
|
||||||
@@ -333,6 +396,23 @@ fun LoadUserProfilePicture(
|
|||||||
innerContent(userProfile?.profilePicture(), userProfile?.bestName())
|
innerContent(userProfile?.profilePicture(), userProfile?.bestName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadUserProfilePicture(
|
||||||
|
baseUserHex: HexKey,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
innerContent: @Composable (String?, String?) -> Unit,
|
||||||
|
) {
|
||||||
|
LoadUser(baseUserHex, accountViewModel) {
|
||||||
|
if (it != null) {
|
||||||
|
val userProfile by observeUserInfo(it, accountViewModel)
|
||||||
|
|
||||||
|
innerContent(userProfile?.profilePicture(), userProfile?.bestName())
|
||||||
|
} else {
|
||||||
|
innerContent(null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InnerUserPicture(
|
fun InnerUserPicture(
|
||||||
userHex: String,
|
userHex: String,
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo
|
|||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe
|
||||||
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
|
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -104,7 +103,7 @@ fun RenderZapNotePreview() {
|
|||||||
user1,
|
user1,
|
||||||
note1,
|
note1,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
EmptyNav,
|
EmptyNav(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +121,7 @@ fun RenderZapNoteSlimPreview() {
|
|||||||
user1,
|
user1,
|
||||||
note1,
|
note1,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
EmptyNav,
|
EmptyNav(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.note.creators.draftTags
|
package com.vitorpamplona.amethyst.ui.note.creators.draftTags
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
@Stable
|
||||||
class DraftTagState {
|
class DraftTagState {
|
||||||
var current: String by mutableStateOf(newTag())
|
var current: String by mutableStateOf(newTag())
|
||||||
var usedDraftTags by mutableStateOf(setOf<String>(current))
|
var usedDraftTags by mutableStateOf(setOf<String>(current))
|
||||||
|
|||||||
+2
-2
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
|
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
|
||||||
@@ -37,7 +37,7 @@ fun WatchAndLoadMyEmojiList(accountViewModel: AccountViewModel) {
|
|||||||
accountViewModel,
|
accountViewModel,
|
||||||
) { emptyNote ->
|
) { emptyNote ->
|
||||||
emptyNote?.let { usersEmojiList ->
|
emptyNote?.let { usersEmojiList ->
|
||||||
val collections by observeNoteEventAndMap(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent ->
|
val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent ->
|
||||||
event.taggedAddresses().toImmutableList()
|
event.taggedAddresses().toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions
|
package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import com.vitorpamplona.amethyst.logTime
|
import com.vitorpamplona.amethyst.logTime
|
||||||
@@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
@Stable
|
||||||
class UserSuggestionState(
|
class UserSuggestionState(
|
||||||
val account: Account,
|
val account: Account,
|
||||||
val requireAtSymbol: Boolean = true,
|
val requireAtSymbol: Boolean = true,
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ fun AddInboxRelayForDMCardPreview() {
|
|||||||
ThemeComparisonColumn {
|
ThemeComparisonColumn {
|
||||||
AddInboxRelayForDMCard(
|
AddInboxRelayForDMCard(
|
||||||
accountViewModel = mockAccountViewModel(),
|
accountViewModel = mockAccountViewModel(),
|
||||||
nav = EmptyNav,
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -138,7 +138,7 @@ fun GenericCommentPostScreen(
|
|||||||
WatchAndLoadMyEmojiList(accountViewModel)
|
WatchAndLoadMyEmojiList(accountViewModel)
|
||||||
|
|
||||||
BackHandler {
|
BackHandler {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendDraftSync()
|
postViewModel.sendDraftSync()
|
||||||
postViewModel.cancel()
|
postViewModel.cancel()
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ fun GenericCommentPostScreen(
|
|||||||
onCancel = {
|
onCancel = {
|
||||||
// uses the accountViewModel scope to avoid cancelling this
|
// uses the accountViewModel scope to avoid cancelling this
|
||||||
// function when the postViewModel is released
|
// function when the postViewModel is released
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendDraftSync()
|
postViewModel.sendDraftSync()
|
||||||
postViewModel.cancel()
|
postViewModel.cancel()
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ fun GenericCommentPostScreen(
|
|||||||
onPost = {
|
onPost = {
|
||||||
// uses the accountViewModel scope to avoid cancelling this
|
// uses the accountViewModel scope to avoid cancelling this
|
||||||
// function when the postViewModel is released
|
// function when the postViewModel is released
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendPostSync()
|
postViewModel.sendPostSync()
|
||||||
nav.popBack()
|
nav.popBack()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,139 +21,214 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note.types
|
package com.vitorpamplona.amethyst.ui.note.types
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap
|
||||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.GalleryUnloaded
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
||||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
|
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
|
||||||
import com.vitorpamplona.amethyst.ui.note.getGradient
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.FollowSetCard
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier
|
import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.blackTagModifier
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayFollowList(
|
fun DisplayFollowList(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
backgroundColor: MutableState<Color>,
|
makeItShort: Boolean,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val noteEvent = baseNote.event as? FollowListEvent ?: return
|
val card =
|
||||||
|
observeNoteEventAndMap(baseNote, accountViewModel) { event: FollowListEvent? ->
|
||||||
var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) }
|
if (event == null) {
|
||||||
|
FollowSetCard(
|
||||||
var expanded by remember { mutableStateOf(false) }
|
name = "",
|
||||||
|
media = "",
|
||||||
val toMembersShow =
|
description = "",
|
||||||
if (expanded) {
|
users = persistentListOf(),
|
||||||
members
|
)
|
||||||
} else {
|
} else {
|
||||||
members.take(3)
|
FollowSetCard(
|
||||||
|
name = event.title()?.ifBlank { null } ?: event.dTag(),
|
||||||
|
media = event.image()?.ifBlank { null },
|
||||||
|
description = event.description(),
|
||||||
|
users = accountViewModel.sortUsersSync(event.followIds()).toImmutableList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val image = noteEvent.image()
|
RenderFollowSetThumbEmbed(
|
||||||
|
card.value,
|
||||||
|
baseNote,
|
||||||
|
makeItShort,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
image?.let {
|
@Composable
|
||||||
|
fun RenderFollowSetThumbEmbed(
|
||||||
|
card: FollowSetCard,
|
||||||
|
baseNote: Note,
|
||||||
|
makeItShort: Boolean,
|
||||||
|
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(
|
MyAsyncImage(
|
||||||
imageUrl = it,
|
imageUrl = it,
|
||||||
contentDescription =
|
contentDescription = stringRes(R.string.preview_card_image_for, it),
|
||||||
stringRes(
|
|
||||||
R.string.preview_card_image_for,
|
|
||||||
it,
|
|
||||||
),
|
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
mainImageModifier = Modifier.fillMaxWidth(),
|
mainImageModifier = Modifier,
|
||||||
loadedImageModifier = FollowSetImageModifier,
|
loadedImageModifier = FollowSetImageModifier,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) },
|
onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) },
|
||||||
onError = { DefaultImageHeader(baseNote, accountViewModel) },
|
onError = { DefaultImageHeader(baseNote, accountViewModel) },
|
||||||
)
|
)
|
||||||
} ?: run {
|
} ?: run { DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier) }
|
||||||
DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier)
|
|
||||||
|
GalleryUnloaded(card.users, StdPadding, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = noteEvent.title() ?: noteEvent.dTag(),
|
text = card.name,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier =
|
modifier = Modifier.weight(1f),
|
||||||
Modifier
|
)
|
||||||
.fillMaxWidth()
|
Text(
|
||||||
.padding(top = 10.dp),
|
text = stringRes(R.string.follow_list_item_label),
|
||||||
textAlign = TextAlign.Center,
|
color = MaterialTheme.colorScheme.background,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = MaterialTheme.colorScheme.blackTagModifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
accountViewModel.loadUsers(noteEvent.taggedUserIds()) {
|
|
||||||
members = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Box {
|
if (!makeItShort) {
|
||||||
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
card.description?.let {
|
||||||
toMembersShow.forEach { user ->
|
val defaultBackground = MaterialTheme.colorScheme.background
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
val background = remember { mutableStateOf(defaultBackground) }
|
||||||
UserCompose(
|
|
||||||
user,
|
TranslatableRichTextViewer(
|
||||||
|
content = it,
|
||||||
|
canPreview = true,
|
||||||
|
quotesLeft = 2,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
tags = baseNote.event?.tags?.toImmutableListOfLists() ?: EmptyTagList,
|
||||||
|
backgroundColor = background,
|
||||||
|
id = it,
|
||||||
|
callbackUri = null,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun RenderFollowSetThumbPreview() {
|
||||||
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
|
val followCard =
|
||||||
|
FollowListEvent(
|
||||||
|
id = "eca31634fce7c9068b56fa8db9f387da70bdcceb3986a77ca1a9844f3128eb5f",
|
||||||
|
pubKey = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da",
|
||||||
|
createdAt = 1761736286,
|
||||||
|
tags =
|
||||||
|
arrayOf(
|
||||||
|
arrayOf("title", "Retro Computer Fans"),
|
||||||
|
arrayOf("d", "xmbspe8rddsq"),
|
||||||
|
arrayOf("image", "https://blog.johnnovak.net/2022/04/15/achieving-period-correct-graphics-in-personal-computer-emulators-part-1-the-amiga/img/dream-setup.jpg"),
|
||||||
|
arrayOf("p", "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da"),
|
||||||
|
arrayOf("p", "9a9a4aa0e43e57873380ab22e8a3df12f3c4cf5bb3a804c6e3fed0069a6e2740"),
|
||||||
|
arrayOf("p", "4f5dd82517b11088ce00f23d99f06fe8f3e2e45ecf47bc9c2f90f34d5c6f7382"),
|
||||||
|
arrayOf("p", "ac92102a2ecb873c488e0125354ef5a97075a16198668c360eda050007ed42cd"),
|
||||||
|
arrayOf("p", "47f54409a4620eb35208a3bc1b53555bf3d0656b246bf0471a93208e20672f6f"),
|
||||||
|
arrayOf("p", "2624911545afb7a2b440cf10f5c69308afa33aae26fca664d8c94623dc0f1baf"),
|
||||||
|
arrayOf("p", "6641f26f5c59f7010dbe3e42e4593398e27c087497cb7d20e0e7633a17e48a94"),
|
||||||
|
arrayOf("description", "Retro computer fans and enthusiasts "),
|
||||||
|
),
|
||||||
|
content = "",
|
||||||
|
sig = "3aa388edafad151e81cb0228fe04e115dbbcaa851c666bfe3c8740b6cd99575f0fc3ba2d47acda86f7626564a05e9dbc05ef452a7bd0ac00f828dbad0e1bae6c",
|
||||||
|
)
|
||||||
|
|
||||||
|
LocalCache.justConsume(followCard, null, false)
|
||||||
|
|
||||||
|
val card =
|
||||||
|
FollowSetCard(
|
||||||
|
name = followCard.title()?.ifBlank { null } ?: followCard.dTag(),
|
||||||
|
media = followCard.image()?.ifBlank { null },
|
||||||
|
description = followCard.description()?.ifBlank { null },
|
||||||
|
users = followCard.followIds().toImmutableList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
ThemeComparisonColumn {
|
||||||
|
RenderFollowSetThumbEmbed(
|
||||||
|
card = card,
|
||||||
|
baseNote = LocalCache.getOrCreateNote(followCard.id),
|
||||||
|
makeItShort = false,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = EmptyNav(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.types
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -36,6 +37,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
@@ -48,9 +50,12 @@ import com.vitorpamplona.amethyst.ui.components.DisplayEvent
|
|||||||
import com.vitorpamplona.amethyst.ui.components.RenderUserAsClickableText
|
import com.vitorpamplona.amethyst.ui.components.RenderUserAsClickableText
|
||||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.components.measureSpaceWidth
|
import com.vitorpamplona.amethyst.ui.components.measureSpaceWidth
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor
|
import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor
|
||||||
@@ -61,6 +66,7 @@ import com.vitorpamplona.quartz.utils.Log
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderHighlight(
|
fun RenderHighlight(
|
||||||
@@ -78,7 +84,7 @@ fun RenderHighlight(
|
|||||||
comment = noteEvent.comment(),
|
comment = noteEvent.comment(),
|
||||||
highlight = noteEvent.quote(),
|
highlight = noteEvent.quote(),
|
||||||
context = noteEvent.context(),
|
context = noteEvent.context(),
|
||||||
authorHex = noteEvent.pubKey,
|
authorHex = noteEvent.author(),
|
||||||
url = noteEvent.inUrl(),
|
url = noteEvent.inUrl(),
|
||||||
postAddress = noteEvent.inPostAddress(),
|
postAddress = noteEvent.inPostAddress(),
|
||||||
postVersion = noteEvent.inPostVersion(),
|
postVersion = noteEvent.inPostVersion(),
|
||||||
@@ -91,6 +97,54 @@ fun RenderHighlight(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DisplayHighlightPreview() {
|
||||||
|
ThemeComparisonColumn {
|
||||||
|
Column {
|
||||||
|
DisplayHighlight(
|
||||||
|
comment = null,
|
||||||
|
highlight = "new architectures of freedom",
|
||||||
|
context = "He never wrote a line of cryptographic code and never lectured on Austrian economics. Yet the cultural terrain he helped seed, particularly the psychedelic, post-industrial counterculture of the 1960s and ’70s, became the moral and metaphysical groundwork from which new architectures of freedom would later emerge.",
|
||||||
|
authorHex = "eaa06714ac905aa5583860391e161edc7a815359b7c3e9b9b202c0558aefbeac",
|
||||||
|
url = null,
|
||||||
|
postAddress = Address(30023, "eaa06714ac905aa5583860391e161edc7a815359b7c3e9b9b202c0558aefbeac", "bitcoin-here-now"),
|
||||||
|
postVersion = null,
|
||||||
|
makeItShort = false,
|
||||||
|
canPreview = true,
|
||||||
|
quotesLeft = 3,
|
||||||
|
backgroundColor = mutableStateOf(Color.White),
|
||||||
|
accountViewModel = mockAccountViewModel(),
|
||||||
|
nav = EmptyNav(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DisplayHighlightPreviewNewLine() {
|
||||||
|
ThemeComparisonColumn {
|
||||||
|
Column {
|
||||||
|
DisplayHighlight(
|
||||||
|
comment = null,
|
||||||
|
highlight = "He never wrote a line of cryptographic code and never lectured on Austrian economics.\nYet the cultural terrain he helped seed, particularly the psychedelic",
|
||||||
|
context = "He never wrote a line of cryptographic code and never lectured on Austrian economics.\nYet the cultural terrain he helped seed, particularly the psychedelic, post-industrial counterculture of the 1960s and ’70s, became the moral and metaphysical groundwork from which new architectures of freedom would later emerge.",
|
||||||
|
authorHex = "eaa06714ac905aa5583860391e161edc7a815359b7c3e9b9b202c0558aefbeac",
|
||||||
|
url = null,
|
||||||
|
postAddress = Address(30023, "eaa06714ac905aa5583860391e161edc7a815359b7c3e9b9b202c0558aefbeac", "bitcoin-here-now"),
|
||||||
|
postVersion = null,
|
||||||
|
makeItShort = false,
|
||||||
|
canPreview = true,
|
||||||
|
quotesLeft = 3,
|
||||||
|
backgroundColor = mutableStateOf(Color.White),
|
||||||
|
accountViewModel = mockAccountViewModel(),
|
||||||
|
nav = EmptyNav(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayHighlight(
|
fun DisplayHighlight(
|
||||||
@@ -124,8 +178,24 @@ fun DisplayHighlight(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val quote =
|
val quote =
|
||||||
remember {
|
remember(highlight) {
|
||||||
highlight.split("\n").joinToString("\n") { "> *${it.removeSuffix(" ")}*" }
|
val uuid = UUID.randomUUID().toString()
|
||||||
|
if (context != null) {
|
||||||
|
if (context.contains(highlight)) {
|
||||||
|
val cleanContext = context.replace(highlight, uuid)
|
||||||
|
|
||||||
|
val quotedContext = cleanContext.split("\n").joinToString("\n") { "> ${it.removeSuffix(" ")}" }
|
||||||
|
|
||||||
|
val quotedSplit = highlight.split("\n")
|
||||||
|
val quotedHighlight = quotedSplit.joinToString("\n >") { "**${it.removeSuffix(" ")}**" }
|
||||||
|
|
||||||
|
quotedContext.replace(uuid, quotedHighlight)
|
||||||
|
} else {
|
||||||
|
highlight.split("\n").joinToString("\n") { "> ${it.removeSuffix(" ")}" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
highlight.split("\n").joinToString("\n") { "> ${it.removeSuffix(" ")}" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TranslatableRichTextViewer(
|
TranslatableRichTextViewer(
|
||||||
@@ -173,9 +243,7 @@ private fun DisplayQuoteAuthor(
|
|||||||
|
|
||||||
if (userBase == null && authorHex != null) {
|
if (userBase == null && authorHex != null) {
|
||||||
LaunchedEffect(authorHex) {
|
LaunchedEffect(authorHex) {
|
||||||
accountViewModel.checkGetOrCreateUser(authorHex) { newUserBase ->
|
userBase = accountViewModel.checkGetOrCreateUser(authorHex)
|
||||||
userBase = newUserBase
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ import java.util.Locale
|
|||||||
fun RenderLiveActivityEventPreview() {
|
fun RenderLiveActivityEventPreview() {
|
||||||
val event = Event.fromJson("{\"id\":\"19406ad34ce3c653d62eb73c1816ac27dcf473c2ccdccf5af7d90d2633c62561\",\"pubkey\":\"6b66886b3add72c779d205be574ec2d7cec619061ac3e75717389b26445989e4\",\"created_at\":1719084750,\"kind\":30311,\"tags\":[[\"r\",\"podcast:guid:72d5e069-f907-5ee7-b0d7-45404f4f0aa5\"],[\"r\",\"podcast:item:guid:bfc33d6e-e00f-4f11-a2ff-94865b7867aa\"],[\"d\",\"bfc33d6e-e00f-4f11-a2ff-94865b7867aa\"],[\"title\",\"The Online Identity Time Bomb\"],[\"summary\",\"Online identity is a ticking time bomb. But are trustworthy, open-source solutions ready to disarm it, or will we be stuck with lackluster, proprietary systems?\\n Live chat: https:/jblive.tv\"],[\"streaming\",\"https://jblive.fm\"],[\"starts\",\"1719167400\"],[\"status\",\"planned\"],[\"image\",\"https://station.us-iad-1.linodeobjects.com/art/lup-mp3.jpg\"]],\"content\":\"\",\"sig\":\"2ce3fae9ad4512541aaae4dbd9484f50df62ab95ba935d7512b736f087f151c1d15ec2bf62d3135474f16c3e070f335b24349c5493461873ddca3051804ca944\"}") as LiveActivitiesEvent
|
val event = Event.fromJson("{\"id\":\"19406ad34ce3c653d62eb73c1816ac27dcf473c2ccdccf5af7d90d2633c62561\",\"pubkey\":\"6b66886b3add72c779d205be574ec2d7cec619061ac3e75717389b26445989e4\",\"created_at\":1719084750,\"kind\":30311,\"tags\":[[\"r\",\"podcast:guid:72d5e069-f907-5ee7-b0d7-45404f4f0aa5\"],[\"r\",\"podcast:item:guid:bfc33d6e-e00f-4f11-a2ff-94865b7867aa\"],[\"d\",\"bfc33d6e-e00f-4f11-a2ff-94865b7867aa\"],[\"title\",\"The Online Identity Time Bomb\"],[\"summary\",\"Online identity is a ticking time bomb. But are trustworthy, open-source solutions ready to disarm it, or will we be stuck with lackluster, proprietary systems?\\n Live chat: https:/jblive.tv\"],[\"streaming\",\"https://jblive.fm\"],[\"starts\",\"1719167400\"],[\"status\",\"planned\"],[\"image\",\"https://station.us-iad-1.linodeobjects.com/art/lup-mp3.jpg\"]],\"content\":\"\",\"sig\":\"2ce3fae9ad4512541aaae4dbd9484f50df62ab95ba935d7512b736f087f151c1d15ec2bf62d3135474f16c3e070f335b24349c5493461873ddca3051804ca944\"}") as LiveActivitiesEvent
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
val baseNote: Note?
|
val baseNote: Note?
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -71,7 +72,7 @@ fun DisplayPeopleList(
|
|||||||
|
|
||||||
var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) }
|
var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) }
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val toMembersShow =
|
val toMembersShow =
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ import kotlin.coroutines.cancellation.CancellationException
|
|||||||
@Composable
|
@Composable
|
||||||
fun TorrentPreview() {
|
fun TorrentPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
val torrent =
|
val torrent =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ import kotlinx.coroutines.withContext
|
|||||||
@Composable
|
@Composable
|
||||||
fun TorrentCommentPreview() {
|
fun TorrentCommentPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
val comment =
|
val comment =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.model.AROUND_ME
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
|
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -72,7 +73,7 @@ class TopNavFilterState(
|
|||||||
val account: Account,
|
val account: Account,
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
val kind3Follow =
|
val allFollows =
|
||||||
PeopleListOutBoxFeedDefinition(
|
PeopleListOutBoxFeedDefinition(
|
||||||
code = ALL_FOLLOWS,
|
code = ALL_FOLLOWS,
|
||||||
name = ResourceName(R.string.follow_list_kind3follows),
|
name = ResourceName(R.string.follow_list_kind3follows),
|
||||||
@@ -81,7 +82,7 @@ class TopNavFilterState(
|
|||||||
unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)),
|
unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val kind3FollowUsers =
|
val userFollows =
|
||||||
PeopleListOutBoxFeedDefinition(
|
PeopleListOutBoxFeedDefinition(
|
||||||
code = ALL_USER_FOLLOWS,
|
code = ALL_USER_FOLLOWS,
|
||||||
name = ResourceName(R.string.follow_list_kind3follows_users_only),
|
name = ResourceName(R.string.follow_list_kind3follows_users_only),
|
||||||
@@ -90,6 +91,15 @@ class TopNavFilterState(
|
|||||||
unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)),
|
unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val kind3Follows =
|
||||||
|
PeopleListOutBoxFeedDefinition(
|
||||||
|
code = KIND3_FOLLOWS,
|
||||||
|
name = ResourceName(R.string.follow_list_kind3_follows_users_only),
|
||||||
|
type = CodeNameType.HARDCODED,
|
||||||
|
kinds = DEFAULT_FEED_KINDS,
|
||||||
|
unpackList = listOf(ContactListEvent.blockListFor(account.signer.pubKey)),
|
||||||
|
)
|
||||||
|
|
||||||
val globalFollow =
|
val globalFollow =
|
||||||
GlobalFeedDefinition(
|
GlobalFeedDefinition(
|
||||||
code = GLOBAL_FOLLOWS,
|
code = GLOBAL_FOLLOWS,
|
||||||
@@ -115,7 +125,7 @@ class TopNavFilterState(
|
|||||||
unpackList = listOf(MuteListEvent.blockListFor(account.userProfile().pubkeyHex)),
|
unpackList = listOf(MuteListEvent.blockListFor(account.userProfile().pubkeyHex)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val defaultLists = persistentListOf(kind3Follow, kind3FollowUsers, aroundMe, globalFollow, muteListFollow)
|
val defaultLists = persistentListOf(allFollows, userFollows, kind3Follows, aroundMe, globalFollow, muteListFollow)
|
||||||
|
|
||||||
fun mergePeopleLists(
|
fun mergePeopleLists(
|
||||||
peopleLists: List<AddressableNote>,
|
peopleLists: List<AddressableNote>,
|
||||||
@@ -229,7 +239,7 @@ class TopNavFilterState(
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
emit(
|
emit(
|
||||||
listOf(
|
listOf(
|
||||||
listOf(kind3Follow, kind3FollowUsers, aroundMe, globalFollow),
|
listOf(allFollows, userFollows, kind3Follows, aroundMe, globalFollow),
|
||||||
myLivePeopleListsFlow,
|
myLivePeopleListsFlow,
|
||||||
myLiveKind3FollowsFlow,
|
myLiveKind3FollowsFlow,
|
||||||
listOf(muteListFollow),
|
listOf(muteListFollow),
|
||||||
@@ -245,7 +255,7 @@ class TopNavFilterState(
|
|||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
emit(
|
emit(
|
||||||
listOf(
|
listOf(
|
||||||
listOf(kind3Follow, kind3FollowUsers, aroundMe, globalFollow),
|
listOf(allFollows, userFollows, kind3Follows, aroundMe, globalFollow),
|
||||||
myLivePeopleListsFlow,
|
myLivePeopleListsFlow,
|
||||||
listOf(muteListFollow),
|
listOf(muteListFollow),
|
||||||
).flatten().toImmutableList(),
|
).flatten().toImmutableList(),
|
||||||
|
|||||||
@@ -21,12 +21,14 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen
|
package com.vitorpamplona.amethyst.ui.screen
|
||||||
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||||
@@ -85,6 +87,7 @@ private fun FeedLoaded(
|
|||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = FeedPadding,
|
contentPadding = FeedPadding,
|
||||||
state = listState,
|
state = listState,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+62
-90
@@ -313,7 +313,7 @@ class AccountViewModel(
|
|||||||
note: Note,
|
note: Note,
|
||||||
reaction: String,
|
reaction: String,
|
||||||
) {
|
) {
|
||||||
runIOCatching {
|
launchSigner {
|
||||||
val currentReactions = note.allReactionsOfContentByAuthor(userProfile(), reaction)
|
val currentReactions = note.allReactionsOfContentByAuthor(userProfile(), reaction)
|
||||||
if (currentReactions.isNotEmpty()) {
|
if (currentReactions.isNotEmpty()) {
|
||||||
account.delete(currentReactions)
|
account.delete(currentReactions)
|
||||||
@@ -679,7 +679,7 @@ class AccountViewModel(
|
|||||||
onProgress: (percent: Float) -> Unit,
|
onProgress: (percent: Float) -> Unit,
|
||||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||||
zapType: LnZapEvent.ZapType? = null,
|
zapType: LnZapEvent.ZapType? = null,
|
||||||
) = runIOCatching {
|
) = launchSigner {
|
||||||
ZapPaymentHandler(account).zap(
|
ZapPaymentHandler(account).zap(
|
||||||
note = note,
|
note = note,
|
||||||
amountMilliSats = amountInMillisats,
|
amountMilliSats = amountInMillisats,
|
||||||
@@ -699,23 +699,23 @@ class AccountViewModel(
|
|||||||
note: Note,
|
note: Note,
|
||||||
type: ReportType,
|
type: ReportType,
|
||||||
content: String = "",
|
content: String = "",
|
||||||
) = runIOCatching { account.report(note, type, content) }
|
) = launchSigner { account.report(note, type, content) }
|
||||||
|
|
||||||
fun report(
|
fun report(
|
||||||
user: User,
|
user: User,
|
||||||
type: ReportType,
|
type: ReportType,
|
||||||
) {
|
) {
|
||||||
runIOCatching {
|
launchSigner {
|
||||||
account.report(user, type)
|
account.report(user, type)
|
||||||
account.hideUser(user.pubkeyHex)
|
account.hideUser(user.pubkeyHex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun boost(note: Note) = runIOCatching { account.boost(note) }
|
fun boost(note: Note) = launchSigner { account.boost(note) }
|
||||||
|
|
||||||
fun removeEmojiPack(emojiPack: Note) = runIOCatching { account.removeEmojiPack(emojiPack) }
|
fun removeEmojiPack(emojiPack: Note) = launchSigner { account.removeEmojiPack(emojiPack) }
|
||||||
|
|
||||||
fun addEmojiPack(emojiPack: Note) = runIOCatching { account.addEmojiPack(emojiPack) }
|
fun addEmojiPack(emojiPack: Note) = launchSigner { account.addEmojiPack(emojiPack) }
|
||||||
|
|
||||||
fun addMediaToGallery(
|
fun addMediaToGallery(
|
||||||
hex: String,
|
hex: String,
|
||||||
@@ -725,40 +725,40 @@ class AccountViewModel(
|
|||||||
dim: DimensionTag?,
|
dim: DimensionTag?,
|
||||||
hash: String?,
|
hash: String?,
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
) = runIOCatching { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) }
|
) = launchSigner { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) }
|
||||||
|
|
||||||
fun removeFromMediaGallery(note: Note) = runIOCatching { account.removeFromGallery(note) }
|
fun removeFromMediaGallery(note: Note) = launchSigner { account.removeFromGallery(note) }
|
||||||
|
|
||||||
fun hashtagFollows(user: User): Note = LocalCache.getOrCreateAddressableNote(HashtagListEvent.createAddress(user.pubkeyHex))
|
fun hashtagFollows(user: User): Note = LocalCache.getOrCreateAddressableNote(HashtagListEvent.createAddress(user.pubkeyHex))
|
||||||
|
|
||||||
fun bookmarks(user: User): Note = LocalCache.getOrCreateAddressableNote(BookmarkListEvent.createBookmarkAddress(user.pubkeyHex))
|
fun bookmarks(user: User): Note = LocalCache.getOrCreateAddressableNote(BookmarkListEvent.createBookmarkAddress(user.pubkeyHex))
|
||||||
|
|
||||||
fun addPrivateBookmark(note: Note) = runIOCatching { account.addBookmark(note, true) }
|
fun addPrivateBookmark(note: Note) = launchSigner { account.addBookmark(note, true) }
|
||||||
|
|
||||||
fun addPublicBookmark(note: Note) = runIOCatching { account.addBookmark(note, false) }
|
fun addPublicBookmark(note: Note) = launchSigner { account.addBookmark(note, false) }
|
||||||
|
|
||||||
fun removePrivateBookmark(note: Note) = runIOCatching { account.removeBookmark(note, true) }
|
fun removePrivateBookmark(note: Note) = launchSigner { account.removeBookmark(note, true) }
|
||||||
|
|
||||||
fun removePublicBookmark(note: Note) = runIOCatching { account.removeBookmark(note, false) }
|
fun removePublicBookmark(note: Note) = launchSigner { account.removeBookmark(note, false) }
|
||||||
|
|
||||||
fun broadcast(note: Note) = runIOCatching { account.broadcast(note) }
|
fun broadcast(note: Note) = launchSigner { account.broadcast(note) }
|
||||||
|
|
||||||
fun timestamp(note: Note) = runIOCatching { account.otsState.timestamp(note) }
|
fun timestamp(note: Note) = launchSigner { account.otsState.timestamp(note) }
|
||||||
|
|
||||||
fun delete(notes: List<Note>) = runIOCatching { account.delete(notes) }
|
fun delete(notes: List<Note>) = launchSigner { account.delete(notes) }
|
||||||
|
|
||||||
fun delete(note: Note) = runIOCatching { account.delete(note) }
|
fun delete(note: Note) = launchSigner { account.delete(note) }
|
||||||
|
|
||||||
fun cachedDecrypt(note: Note): String? = account.cachedDecryptContent(note)
|
fun cachedDecrypt(note: Note): String? = account.cachedDecryptContent(note)
|
||||||
|
|
||||||
fun decrypt(
|
fun decrypt(
|
||||||
note: Note,
|
note: Note,
|
||||||
onReady: (String) -> Unit,
|
onReady: (String) -> Unit,
|
||||||
) = runIOCatching {
|
) = launchSigner {
|
||||||
account.decryptContent(note)?.let { onReady(it) }
|
account.decryptContent(note)?.let { onReady(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun runIOCatching(crossinline action: suspend () -> Unit) {
|
inline fun launchSigner(crossinline action: suspend () -> Unit) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
action()
|
action()
|
||||||
@@ -796,35 +796,35 @@ class AccountViewModel(
|
|||||||
fun approveCommunityPost(
|
fun approveCommunityPost(
|
||||||
post: Note,
|
post: Note,
|
||||||
community: AddressableNote,
|
community: AddressableNote,
|
||||||
) = runIOCatching { account.approveCommunityPost(post, community) }
|
) = launchSigner { account.approveCommunityPost(post, community) }
|
||||||
|
|
||||||
fun follow(community: AddressableNote) = runIOCatching { account.follow(community) }
|
fun follow(community: AddressableNote) = launchSigner { account.follow(community) }
|
||||||
|
|
||||||
fun follow(channel: PublicChatChannel) = runIOCatching { account.follow(channel) }
|
fun follow(channel: PublicChatChannel) = launchSigner { account.follow(channel) }
|
||||||
|
|
||||||
fun follow(channel: EphemeralChatChannel) = runIOCatching { account.follow(channel) }
|
fun follow(channel: EphemeralChatChannel) = launchSigner { account.follow(channel) }
|
||||||
|
|
||||||
fun unfollow(community: AddressableNote) = runIOCatching { account.unfollow(community) }
|
fun unfollow(community: AddressableNote) = launchSigner { account.unfollow(community) }
|
||||||
|
|
||||||
fun unfollow(channel: PublicChatChannel) = runIOCatching { account.unfollow(channel) }
|
fun unfollow(channel: PublicChatChannel) = launchSigner { account.unfollow(channel) }
|
||||||
|
|
||||||
fun unfollow(channel: EphemeralChatChannel) = runIOCatching { account.unfollow(channel) }
|
fun unfollow(channel: EphemeralChatChannel) = launchSigner { account.unfollow(channel) }
|
||||||
|
|
||||||
fun follow(user: User) = runIOCatching { account.follow(user) }
|
fun follow(user: User) = launchSigner { account.follow(user) }
|
||||||
|
|
||||||
fun unfollow(user: User) = runIOCatching { account.unfollow(user) }
|
fun unfollow(user: User) = launchSigner { account.unfollow(user) }
|
||||||
|
|
||||||
fun followGeohash(tag: String) = runIOCatching { account.followGeohash(tag) }
|
fun followGeohash(tag: String) = launchSigner { account.followGeohash(tag) }
|
||||||
|
|
||||||
fun unfollowGeohash(tag: String) = runIOCatching { account.unfollowGeohash(tag) }
|
fun unfollowGeohash(tag: String) = launchSigner { account.unfollowGeohash(tag) }
|
||||||
|
|
||||||
fun followHashtag(tag: String) = runIOCatching { account.followHashtag(tag) }
|
fun followHashtag(tag: String) = launchSigner { account.followHashtag(tag) }
|
||||||
|
|
||||||
fun unfollowHashtag(tag: String) = runIOCatching { account.unfollowHashtag(tag) }
|
fun unfollowHashtag(tag: String) = launchSigner { account.unfollowHashtag(tag) }
|
||||||
|
|
||||||
fun showWord(word: String) = runIOCatching { account.showWord(word) }
|
fun showWord(word: String) = launchSigner { account.showWord(word) }
|
||||||
|
|
||||||
fun hideWord(word: String) = runIOCatching { account.hideWord(word) }
|
fun hideWord(word: String) = launchSigner { account.hideWord(word) }
|
||||||
|
|
||||||
fun isLoggedUser(pubkeyHex: HexKey?): Boolean = account.signer.pubKey == pubkeyHex
|
fun isLoggedUser(pubkeyHex: HexKey?): Boolean = account.signer.pubKey == pubkeyHex
|
||||||
|
|
||||||
@@ -859,21 +859,21 @@ class AccountViewModel(
|
|||||||
|
|
||||||
fun filterSpamFromStrangers() = account.settings.syncedSettings.security.filterSpamFromStrangers
|
fun filterSpamFromStrangers() = account.settings.syncedSettings.security.filterSpamFromStrangers
|
||||||
|
|
||||||
fun updateWarnReports(warnReports: Boolean) = runIOCatching { account.updateWarnReports(warnReports) }
|
fun updateWarnReports(warnReports: Boolean) = launchSigner { account.updateWarnReports(warnReports) }
|
||||||
|
|
||||||
fun updateFilterSpam(filterSpam: Boolean) =
|
fun updateFilterSpam(filterSpam: Boolean) =
|
||||||
runIOCatching {
|
launchSigner {
|
||||||
if (account.updateFilterSpam(filterSpam)) {
|
if (account.updateFilterSpam(filterSpam)) {
|
||||||
LocalCache.antiSpam.active = filterSpamFromStrangers().value
|
LocalCache.antiSpam.active = filterSpamFromStrangers().value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateShowSensitiveContent(show: Boolean?) = runIOCatching { account.updateShowSensitiveContent(show) }
|
fun updateShowSensitiveContent(show: Boolean?) = launchSigner { account.updateShowSensitiveContent(show) }
|
||||||
|
|
||||||
fun changeReactionTypes(
|
fun changeReactionTypes(
|
||||||
reactionSet: List<String>,
|
reactionSet: List<String>,
|
||||||
onDone: () -> Unit,
|
onDone: () -> Unit,
|
||||||
) = runIOCatching {
|
) = launchSigner {
|
||||||
account.changeReactionTypes(reactionSet)
|
account.changeReactionTypes(reactionSet)
|
||||||
onDone()
|
onDone()
|
||||||
}
|
}
|
||||||
@@ -882,37 +882,37 @@ class AccountViewModel(
|
|||||||
amountSet: List<Long>,
|
amountSet: List<Long>,
|
||||||
selectedZapType: LnZapEvent.ZapType,
|
selectedZapType: LnZapEvent.ZapType,
|
||||||
nip47Update: Nip47WalletConnect.Nip47URINorm?,
|
nip47Update: Nip47WalletConnect.Nip47URINorm?,
|
||||||
) = runIOCatching { account.updateZapAmounts(amountSet, selectedZapType, nip47Update) }
|
) = launchSigner { account.updateZapAmounts(amountSet, selectedZapType, nip47Update) }
|
||||||
|
|
||||||
fun toggleDontTranslateFrom(languageCode: String) = runIOCatching { account.toggleDontTranslateFrom(languageCode) }
|
fun toggleDontTranslateFrom(languageCode: String) = launchSigner { account.toggleDontTranslateFrom(languageCode) }
|
||||||
|
|
||||||
fun updateTranslateTo(languageCode: Locale) = runIOCatching { account.updateTranslateTo(languageCode) }
|
fun updateTranslateTo(languageCode: Locale) = launchSigner { account.updateTranslateTo(languageCode) }
|
||||||
|
|
||||||
fun prefer(
|
fun prefer(
|
||||||
source: String,
|
source: String,
|
||||||
target: String,
|
target: String,
|
||||||
preference: String,
|
preference: String,
|
||||||
) = runIOCatching { account.prefer(source, target, preference) }
|
) = launchSigner { account.prefer(source, target, preference) }
|
||||||
|
|
||||||
fun show(user: User) = runIOCatching { account.showUser(user.pubkeyHex) }
|
fun show(user: User) = launchSigner { account.showUser(user.pubkeyHex) }
|
||||||
|
|
||||||
fun hide(user: User) = runIOCatching { account.hideUser(user.pubkeyHex) }
|
fun hide(user: User) = launchSigner { account.hideUser(user.pubkeyHex) }
|
||||||
|
|
||||||
fun hide(word: String) = runIOCatching { account.hideWord(word) }
|
fun hide(word: String) = launchSigner { account.hideWord(word) }
|
||||||
|
|
||||||
fun showUser(pubkeyHex: String) = runIOCatching { account.showUser(pubkeyHex) }
|
fun showUser(pubkeyHex: String) = launchSigner { account.showUser(pubkeyHex) }
|
||||||
|
|
||||||
fun createStatus(newStatus: String) = runIOCatching { account.createStatus(newStatus) }
|
fun createStatus(newStatus: String) = launchSigner { account.createStatus(newStatus) }
|
||||||
|
|
||||||
fun updateStatus(
|
fun updateStatus(
|
||||||
address: Address,
|
address: Address,
|
||||||
newStatus: String,
|
newStatus: String,
|
||||||
) = runIOCatching {
|
) = launchSigner {
|
||||||
account.updateStatus(LocalCache.getOrCreateAddressableNote(address), newStatus)
|
account.updateStatus(LocalCache.getOrCreateAddressableNote(address), newStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteStatus(address: Address) =
|
fun deleteStatus(address: Address) =
|
||||||
runIOCatching {
|
launchSigner {
|
||||||
account.deleteStatus(LocalCache.getOrCreateAddressableNote(address))
|
account.deleteStatus(LocalCache.getOrCreateAddressableNote(address))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,31 +977,13 @@ class AccountViewModel(
|
|||||||
|
|
||||||
override suspend fun getOrCreateUser(hex: HexKey): User = LocalCache.getOrCreateUser(hex)
|
override suspend fun getOrCreateUser(hex: HexKey): User = LocalCache.getOrCreateUser(hex)
|
||||||
|
|
||||||
fun checkGetOrCreateUser(
|
|
||||||
key: HexKey,
|
|
||||||
onResult: (User?) -> Unit,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateUser(key)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUserIfExists(hex: HexKey): User? = LocalCache.getUserIfExists(hex)
|
fun getUserIfExists(hex: HexKey): User? = LocalCache.getUserIfExists(hex)
|
||||||
|
|
||||||
fun checkGetOrCreateNote(key: HexKey): Note? = LocalCache.checkGetOrCreateNote(key)
|
fun checkGetOrCreateNote(key: HexKey): Note? = LocalCache.checkGetOrCreateNote(key)
|
||||||
|
|
||||||
override suspend fun getOrCreateNote(hex: HexKey): Note = LocalCache.getOrCreateNote(hex)
|
override suspend fun getOrCreateNote(hex: HexKey): Note = LocalCache.getOrCreateNote(hex)
|
||||||
|
|
||||||
fun checkGetOrCreateNote(
|
fun noteFromEvent(event: Event): Note? {
|
||||||
key: HexKey,
|
|
||||||
onResult: (Note?) -> Unit,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateNote(key)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkGetOrCreateNote(
|
|
||||||
event: Event,
|
|
||||||
onResult: (Note?) -> Unit,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
var note = checkGetOrCreateNote(event.id)
|
var note = checkGetOrCreateNote(event.id)
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
@@ -1009,21 +991,13 @@ class AccountViewModel(
|
|||||||
note = checkGetOrCreateNote(event.id)
|
note = checkGetOrCreateNote(event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
onResult(note)
|
return note
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex)
|
fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex)
|
||||||
|
|
||||||
override suspend fun getOrCreateAddressableNote(address: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(address)
|
override suspend fun getOrCreateAddressableNote(address: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(address)
|
||||||
|
|
||||||
fun getOrCreateAddressableNote(
|
|
||||||
key: Address,
|
|
||||||
onResult: (AddressableNote?) -> Unit,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) { onResult(getOrCreateAddressableNote(key)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key)
|
fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key)
|
||||||
|
|
||||||
fun getAddressableNoteIfExists(key: Address): AddressableNote? = LocalCache.getAddressableNoteIfExists(key)
|
fun getAddressableNoteIfExists(key: Address): AddressableNote? = LocalCache.getAddressableNoteIfExists(key)
|
||||||
@@ -1076,11 +1050,12 @@ class AccountViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sortUsersSync(hexList: List<HexKey>): List<HexKey> = hexList.sortedByDescending { account.isKnown(it) }
|
||||||
|
|
||||||
fun loadUsersSync(hexList: List<String>): List<User> =
|
fun loadUsersSync(hexList: List<String>): List<User> =
|
||||||
hexList
|
hexList
|
||||||
.mapNotNull { hex -> checkGetOrCreateUser(hex) }
|
.mapNotNull { hex -> checkGetOrCreateUser(hex) }
|
||||||
.sortedBy { account.isFollowing(it) }
|
.sortedByDescending { account.isKnown(it) }
|
||||||
.reversed()
|
|
||||||
|
|
||||||
suspend fun checkVideoIsOnline(videoUrl: String): Boolean =
|
suspend fun checkVideoIsOnline(videoUrl: String): Boolean =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -1177,10 +1152,9 @@ class AccountViewModel(
|
|||||||
context: Context,
|
context: Context,
|
||||||
) {
|
) {
|
||||||
if (isWriteable()) {
|
if (isWriteable()) {
|
||||||
val hint = note.toEventHint<VoiceEvent>()
|
val hint = note.toEventHint<VoiceEvent>() ?: return
|
||||||
if (hint == null) return
|
|
||||||
|
|
||||||
runIOCatching {
|
launchSigner {
|
||||||
val uploader = UploadOrchestrator()
|
val uploader = UploadOrchestrator()
|
||||||
val result =
|
val result =
|
||||||
uploader.upload(
|
uploader.upload(
|
||||||
@@ -1273,7 +1247,7 @@ class AccountViewModel(
|
|||||||
if (isWriteable()) {
|
if (isWriteable()) {
|
||||||
val boosts = baseNote.boostedBy(userProfile())
|
val boosts = baseNote.boostedBy(userProfile())
|
||||||
if (boosts.isNotEmpty()) {
|
if (boosts.isNotEmpty()) {
|
||||||
runIOCatching {
|
launchSigner {
|
||||||
account.delete(boosts)
|
account.delete(boosts)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1335,9 +1309,7 @@ class AccountViewModel(
|
|||||||
if (existingNoteEvent != null) {
|
if (existingNoteEvent != null) {
|
||||||
unwrapIfNeeded(existingNoteEvent)
|
unwrapIfNeeded(existingNoteEvent)
|
||||||
} else {
|
} else {
|
||||||
val newEvent = event.unwrapOrNull(account.signer)
|
val newEvent = event.unwrapOrNull(account.signer) ?: return null
|
||||||
|
|
||||||
if (newEvent == null) return null
|
|
||||||
|
|
||||||
// clear the encrypted payload to save memory
|
// clear the encrypted payload to save memory
|
||||||
LocalCache.getOrCreateNote(event.id).event = event.copyNoContent()
|
LocalCache.getOrCreateNote(event.id).event = event.copyNoContent()
|
||||||
@@ -1403,7 +1375,7 @@ class AccountViewModel(
|
|||||||
fun unwrapIfNeeded(
|
fun unwrapIfNeeded(
|
||||||
note: Note?,
|
note: Note?,
|
||||||
onReady: (Note) -> Unit = {},
|
onReady: (Note) -> Unit = {},
|
||||||
) = runIOCatching {
|
) = launchSigner {
|
||||||
val noteEvent = note?.event
|
val noteEvent = note?.event
|
||||||
if (noteEvent != null) {
|
if (noteEvent != null) {
|
||||||
val resultingNote = unwrapIfNeeded(noteEvent)
|
val resultingNote = unwrapIfNeeded(noteEvent)
|
||||||
@@ -1435,7 +1407,7 @@ class AccountViewModel(
|
|||||||
dvmPublicKey: User,
|
dvmPublicKey: User,
|
||||||
onReady: (event: Note) -> Unit,
|
onReady: (event: Note) -> Unit,
|
||||||
) {
|
) {
|
||||||
runIOCatching {
|
launchSigner {
|
||||||
account.requestDVMContentDiscovery(dvmPublicKey) {
|
account.requestDVMContentDiscovery(dvmPublicKey) {
|
||||||
onReady(LocalCache.getOrCreateNote(it.id))
|
onReady(LocalCache.getOrCreateNote(it.id))
|
||||||
}
|
}
|
||||||
@@ -1469,7 +1441,7 @@ class AccountViewModel(
|
|||||||
zappedNote: Note?,
|
zappedNote: Note?,
|
||||||
onSent: () -> Unit = {},
|
onSent: () -> Unit = {},
|
||||||
onResponse: (Response?) -> Unit,
|
onResponse: (Response?) -> Unit,
|
||||||
) = runIOCatching {
|
) = launchSigner {
|
||||||
account.sendZapPaymentRequestFor(bolt11, zappedNote, onResponse)
|
account.sendZapPaymentRequestFor(bolt11, zappedNote, onResponse)
|
||||||
onSent()
|
onSent()
|
||||||
}
|
}
|
||||||
@@ -1480,7 +1452,7 @@ class AccountViewModel(
|
|||||||
root: InteractiveStoryBaseEvent,
|
root: InteractiveStoryBaseEvent,
|
||||||
readingScene: InteractiveStoryBaseEvent,
|
readingScene: InteractiveStoryBaseEvent,
|
||||||
) {
|
) {
|
||||||
runIOCatching {
|
launchSigner {
|
||||||
val sceneNoteRelayHint = LocalCache.getOrCreateAddressableNote(readingScene.address()).relayHintUrl()
|
val sceneNoteRelayHint = LocalCache.getOrCreateAddressableNote(readingScene.address()).relayHintUrl()
|
||||||
|
|
||||||
val readingState = getInteractiveStoryReadingState(root.addressTag())
|
val readingState = getInteractiveStoryReadingState(root.addressTag())
|
||||||
|
|||||||
+1
-1
@@ -117,7 +117,7 @@ fun RenderChannelDataPreview() {
|
|||||||
tags = EmptyTagList,
|
tags = EmptyTagList,
|
||||||
bgColor = remember { mutableStateOf(Color.Transparent) },
|
bgColor = remember { mutableStateOf(Color.Transparent) },
|
||||||
accountViewModel = mockAccountViewModel(),
|
accountViewModel = mockAccountViewModel(),
|
||||||
nav = EmptyNav,
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-8
@@ -72,19 +72,17 @@ fun ChatroomView(
|
|||||||
|
|
||||||
if (replyToNote != null) {
|
if (replyToNote != null) {
|
||||||
LaunchedEffect(key1 = replyToNote, newPostModel, accountViewModel) {
|
LaunchedEffect(key1 = replyToNote, newPostModel, accountViewModel) {
|
||||||
accountViewModel.checkGetOrCreateNote(replyToNote) {
|
val replyNote = accountViewModel.checkGetOrCreateNote(replyToNote)
|
||||||
if (it != null) {
|
if (replyNote != null) {
|
||||||
newPostModel.reply(it)
|
newPostModel.reply(replyNote)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (editFromDraft != null) {
|
if (editFromDraft != null) {
|
||||||
LaunchedEffect(editFromDraft, newPostModel, accountViewModel) {
|
LaunchedEffect(editFromDraft, newPostModel, accountViewModel) {
|
||||||
accountViewModel.checkGetOrCreateNote(editFromDraft) {
|
val draftNote = accountViewModel.checkGetOrCreateNote(editFromDraft)
|
||||||
if (it != null) {
|
if (draftNote != null) {
|
||||||
newPostModel.editFromDraft(it)
|
newPostModel.editFromDraft(draftNote)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -98,7 +98,7 @@ fun NewChatroomSubjectDialog(
|
|||||||
|
|
||||||
PostButton(
|
PostButton(
|
||||||
onPost = {
|
onPost = {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
val template =
|
val template =
|
||||||
ChatMessageEvent.build(
|
ChatMessageEvent.build(
|
||||||
message.value,
|
message.value,
|
||||||
|
|||||||
+5
-9
@@ -114,7 +114,7 @@ class ChatNewMessageViewModel :
|
|||||||
draftTag.versions.collectLatest {
|
draftTag.versions.collectLatest {
|
||||||
// don't save the first
|
// don't save the first
|
||||||
if (it > 0) {
|
if (it > 0) {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
sendDraftSync()
|
sendDraftSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,9 +325,7 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (replyId != null) {
|
if (replyId != null) {
|
||||||
accountViewModel.checkGetOrCreateNote(replyId) {
|
replyTo.value = accountViewModel.checkGetOrCreateNote(replyId)
|
||||||
replyTo.value = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (draftEvent is PrivateDmEvent) {
|
} else if (draftEvent is PrivateDmEvent) {
|
||||||
val recipientNPub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
val recipientNPub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
||||||
@@ -335,9 +333,7 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
val replyId = draftEvent.replyTo()
|
val replyId = draftEvent.replyTo()
|
||||||
if (replyId != null) {
|
if (replyId != null) {
|
||||||
accountViewModel.checkGetOrCreateNote(replyId) {
|
replyTo.value = accountViewModel.checkGetOrCreateNote(replyId)
|
||||||
replyTo.value = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +389,7 @@ class ChatNewMessageViewModel :
|
|||||||
) {
|
) {
|
||||||
val uploadState = uploadState ?: return
|
val uploadState = uploadState ?: return
|
||||||
|
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
if (nip17) {
|
if (nip17) {
|
||||||
ChatFileUploader(account).justUploadNIP17(uploadState, onError, context) {
|
ChatFileUploader(account).justUploadNIP17(uploadState, onError, context) {
|
||||||
uploadsWaitingToBeSent += it
|
uploadsWaitingToBeSent += it
|
||||||
@@ -418,7 +414,7 @@ class ChatNewMessageViewModel :
|
|||||||
val room = room ?: return
|
val room = room ?: return
|
||||||
val uploadState = uploadState ?: return
|
val uploadState = uploadState ?: return
|
||||||
|
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
if (nip17) {
|
if (nip17) {
|
||||||
ChatFileUploader(account).justUploadNIP17(uploadState, onError, context) {
|
ChatFileUploader(account).justUploadNIP17(uploadState, onError, context) {
|
||||||
ChatFileSender(room, account).sendNIP17(it)
|
ChatFileSender(room, account).sendNIP17(it)
|
||||||
|
|||||||
+3
-3
@@ -169,7 +169,7 @@ fun NewGroupDMScreen(
|
|||||||
WatchAndLoadMyEmojiList(accountViewModel)
|
WatchAndLoadMyEmojiList(accountViewModel)
|
||||||
|
|
||||||
BackHandler {
|
BackHandler {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendDraftSync()
|
postViewModel.sendDraftSync()
|
||||||
postViewModel.cancel()
|
postViewModel.cancel()
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ fun NewGroupDMScreen(
|
|||||||
onCancel = {
|
onCancel = {
|
||||||
// uses the accountViewModel scope to avoid cancelling this
|
// uses the accountViewModel scope to avoid cancelling this
|
||||||
// function when the postViewModel is released
|
// function when the postViewModel is released
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendDraftSync()
|
postViewModel.sendDraftSync()
|
||||||
postViewModel.cancel()
|
postViewModel.cancel()
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,7 @@ fun NewGroupDMScreen(
|
|||||||
onPost = {
|
onPost = {
|
||||||
// uses the accountViewModel scope to avoid cancelling this
|
// uses the accountViewModel scope to avoid cancelling this
|
||||||
// function when the postViewModel is released
|
// function when the postViewModel is released
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendPostSync()
|
postViewModel.sendPostSync()
|
||||||
postViewModel.room?.let {
|
postViewModel.room?.let {
|
||||||
nav.nav(routeToMessage(it, null, null, null, null, accountViewModel))
|
nav.nav(routeToMessage(it, null, null, null, null, accountViewModel))
|
||||||
|
|||||||
+3
-3
@@ -72,7 +72,7 @@ fun PrivateMessageEditFieldRowPreview() {
|
|||||||
channelScreenModel = channelScreenModel,
|
channelScreenModel = channelScreenModel,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onSendNewMessage = {},
|
onSendNewMessage = {},
|
||||||
nav = EmptyNav,
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ fun PrivateMessageEditFieldRow(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
BackHandler {
|
BackHandler {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
channelScreenModel.sendDraftSync()
|
channelScreenModel.sendDraftSync()
|
||||||
channelScreenModel.cancel()
|
channelScreenModel.cancel()
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ fun EditField(
|
|||||||
isActive = channelScreenModel.canPost(),
|
isActive = channelScreenModel.canPost(),
|
||||||
modifier = EditFieldTrailingIconModifier,
|
modifier = EditFieldTrailingIconModifier,
|
||||||
) {
|
) {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
channelScreenModel.sendPostSync()
|
channelScreenModel.sendPostSync()
|
||||||
onSendNewMessage()
|
onSendNewMessage()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -81,7 +81,7 @@ private fun DialogContentPreview() {
|
|||||||
ChannelMetadataScaffold(
|
ChannelMetadataScaffold(
|
||||||
postViewModel = postViewModel,
|
postViewModel = postViewModel,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = EmptyNav,
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-3
@@ -36,6 +36,7 @@ import androidx.compose.material3.OutlinedTextField
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -91,7 +92,17 @@ fun ChannelMetadataScreen(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val postViewModel: ChannelMetadataViewModel = viewModel()
|
val postViewModel: ChannelMetadataViewModel = viewModel()
|
||||||
postViewModel.load(accountViewModel.account, channel)
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
|
if (channel != null) {
|
||||||
|
LaunchedEffect(postViewModel) {
|
||||||
|
postViewModel.load(channel)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(postViewModel) {
|
||||||
|
postViewModel.new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ChannelMetadataScaffold(
|
ChannelMetadataScaffold(
|
||||||
postViewModel = postViewModel,
|
postViewModel = postViewModel,
|
||||||
@@ -105,13 +116,13 @@ fun ChannelMetadataScreen(
|
|||||||
private fun DialogContentPreview() {
|
private fun DialogContentPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
val postViewModel: ChannelMetadataViewModel = viewModel()
|
val postViewModel: ChannelMetadataViewModel = viewModel()
|
||||||
postViewModel.load(accountViewModel.account, null)
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
ThemeComparisonColumn {
|
ThemeComparisonColumn {
|
||||||
ChannelMetadataScaffold(
|
ChannelMetadataScaffold(
|
||||||
postViewModel = postViewModel,
|
postViewModel = postViewModel,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = EmptyNav,
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-8
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -39,6 +40,7 @@ import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
|||||||
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -54,8 +56,11 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
@Stable
|
||||||
class ChannelMetadataViewModel : ViewModel() {
|
class ChannelMetadataViewModel : ViewModel() {
|
||||||
private var account: Account? = null
|
private lateinit var accountViewModel: AccountViewModel
|
||||||
|
private lateinit var account: Account
|
||||||
|
|
||||||
private var originalChannel: PublicChatChannel? = null
|
private var originalChannel: PublicChatChannel? = null
|
||||||
|
|
||||||
val channelName = mutableStateOf(TextFieldValue())
|
val channelName = mutableStateOf(TextFieldValue())
|
||||||
@@ -71,12 +76,17 @@ class ChannelMetadataViewModel : ViewModel() {
|
|||||||
channelName.value.text.isNotBlank()
|
channelName.value.text.isNotBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load(
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
account: Account,
|
this.accountViewModel = accountViewModel
|
||||||
channel: PublicChatChannel?,
|
this.account = accountViewModel.account
|
||||||
) {
|
}
|
||||||
this.account = account
|
|
||||||
if (channel != null) {
|
fun new() {
|
||||||
|
originalChannel = null
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(channel: PublicChatChannel) {
|
||||||
originalChannel = channel
|
originalChannel = channel
|
||||||
channelName.value = TextFieldValue(channel.info.name ?: "")
|
channelName.value = TextFieldValue(channel.info.name ?: "")
|
||||||
channelPicture.value = TextFieldValue(channel.info.picture ?: "")
|
channelPicture.value = TextFieldValue(channel.info.picture ?: "")
|
||||||
@@ -89,7 +99,6 @@ class ChannelMetadataViewModel : ViewModel() {
|
|||||||
|
|
||||||
_channelRelays.update { relays ?: emptyList() }
|
_channelRelays.update { relays ?: emptyList() }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun isNewChannel() = originalChannel == null && _channelRelays.value.isNotEmpty()
|
fun isNewChannel() = originalChannel == null && _channelRelays.value.isNotEmpty()
|
||||||
|
|
||||||
|
|||||||
+4
-8
@@ -107,7 +107,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
draftTag.versions.collectLatest {
|
draftTag.versions.collectLatest {
|
||||||
// don't save the first
|
// don't save the first
|
||||||
if (it > 0) {
|
if (it > 0) {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
sendDraftSync()
|
sendDraftSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,16 +242,12 @@ open class ChannelNewMessageViewModel :
|
|||||||
if (draftEvent as? ChannelMessageEvent != null) {
|
if (draftEvent as? ChannelMessageEvent != null) {
|
||||||
val replyId = draftEvent.reply()?.eventId
|
val replyId = draftEvent.reply()?.eventId
|
||||||
if (replyId != null) {
|
if (replyId != null) {
|
||||||
accountViewModel.checkGetOrCreateNote(replyId) {
|
replyTo.value = accountViewModel.checkGetOrCreateNote(replyId)
|
||||||
replyTo.value = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (draftEvent as? LiveActivitiesChatMessageEvent != null) {
|
} else if (draftEvent as? LiveActivitiesChatMessageEvent != null) {
|
||||||
val replyId = draftEvent.reply()?.eventId
|
val replyId = draftEvent.reply()?.eventId
|
||||||
if (replyId != null) {
|
if (replyId != null) {
|
||||||
accountViewModel.checkGetOrCreateNote(replyId) {
|
replyTo.value = accountViewModel.checkGetOrCreateNote(replyId)
|
||||||
replyTo.value = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +259,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendPost(onDone: suspend () -> Unit) {
|
fun sendPost(onDone: suspend () -> Unit) {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
sendPostSync()
|
sendPostSync()
|
||||||
onDone()
|
onDone()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ fun EditFieldRow(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
BackHandler {
|
BackHandler {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
channelScreenModel.sendDraftSync()
|
channelScreenModel.sendDraftSync()
|
||||||
channelScreenModel.cancel()
|
channelScreenModel.cancel()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -327,11 +327,7 @@ fun LoadUser(
|
|||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
LaunchedEffect(key1 = baseUserHex) {
|
LaunchedEffect(key1 = baseUserHex) {
|
||||||
accountViewModel.checkGetOrCreateUser(baseUserHex) { newUser ->
|
user = accountViewModel.checkGetOrCreateUser(baseUserHex)
|
||||||
if (user != newUser) {
|
|
||||||
user = newUser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ private fun TopNavFilterBar(
|
|||||||
placeholderCode = listName,
|
placeholderCode = listName,
|
||||||
explainer = stringRes(R.string.select_list_to_filter),
|
explainer = stringRes(R.string.select_list_to_filter),
|
||||||
options = allLists,
|
options = allLists,
|
||||||
onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow) },
|
onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.allFollows) },
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-27
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -39,13 +38,13 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
|
||||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.Gallery
|
import com.vitorpamplona.amethyst.ui.note.GalleryUnloaded
|
||||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
@@ -58,13 +57,13 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier
|
import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
|
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -72,7 +71,7 @@ data class FollowSetCard(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val media: String?,
|
val media: String?,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val users: ImmutableList<User>,
|
val users: ImmutableList<HexKey>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -90,7 +89,7 @@ fun RenderFollowSetThumb(
|
|||||||
description = noteEvent?.description(),
|
description = noteEvent?.description(),
|
||||||
users =
|
users =
|
||||||
accountViewModel
|
accountViewModel
|
||||||
.loadUsersSync(
|
.sortUsersSync(
|
||||||
noteEvent?.followIds() ?: emptyList(),
|
noteEvent?.followIds() ?: emptyList(),
|
||||||
).toImmutableList(),
|
).toImmutableList(),
|
||||||
)
|
)
|
||||||
@@ -108,30 +107,49 @@ fun RenderFollowSetThumb(
|
|||||||
@Composable
|
@Composable
|
||||||
fun RenderFollowSetThumbPreview() {
|
fun RenderFollowSetThumbPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
val nav = EmptyNav
|
val nav = EmptyNav()
|
||||||
|
|
||||||
ThemeComparisonColumn(
|
val followCard =
|
||||||
toPreview = {
|
FollowListEvent(
|
||||||
RenderFollowSetThumb(
|
id = "eca31634fce7c9068b56fa8db9f387da70bdcceb3986a77ca1a9844f3128eb5f",
|
||||||
card =
|
pubKey = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da",
|
||||||
|
createdAt = 1761736286,
|
||||||
|
tags =
|
||||||
|
arrayOf(
|
||||||
|
arrayOf("title", "Retro Computer Fans"),
|
||||||
|
arrayOf("d", "xmbspe8rddsq"),
|
||||||
|
arrayOf("image", "https://blog.johnnovak.net/2022/04/15/achieving-period-correct-graphics-in-personal-computer-emulators-part-1-the-amiga/img/dream-setup.jpg"),
|
||||||
|
arrayOf("p", "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da"),
|
||||||
|
arrayOf("p", "9a9a4aa0e43e57873380ab22e8a3df12f3c4cf5bb3a804c6e3fed0069a6e2740"),
|
||||||
|
arrayOf("p", "4f5dd82517b11088ce00f23d99f06fe8f3e2e45ecf47bc9c2f90f34d5c6f7382"),
|
||||||
|
arrayOf("p", "ac92102a2ecb873c488e0125354ef5a97075a16198668c360eda050007ed42cd"),
|
||||||
|
arrayOf("p", "47f54409a4620eb35208a3bc1b53555bf3d0656b246bf0471a93208e20672f6f"),
|
||||||
|
arrayOf("p", "2624911545afb7a2b440cf10f5c69308afa33aae26fca664d8c94623dc0f1baf"),
|
||||||
|
arrayOf("p", "6641f26f5c59f7010dbe3e42e4593398e27c087497cb7d20e0e7633a17e48a94"),
|
||||||
|
arrayOf("description", "Retro computer fans and enthusiasts "),
|
||||||
|
),
|
||||||
|
content = "",
|
||||||
|
sig = "3aa388edafad151e81cb0228fe04e115dbbcaa851c666bfe3c8740b6cd99575f0fc3ba2d47acda86f7626564a05e9dbc05ef452a7bd0ac00f828dbad0e1bae6c",
|
||||||
|
)
|
||||||
|
|
||||||
|
LocalCache.justConsume(followCard, null, false)
|
||||||
|
|
||||||
|
val card =
|
||||||
FollowSetCard(
|
FollowSetCard(
|
||||||
"Orange Pill Perú",
|
name = followCard.title()?.ifBlank { null } ?: followCard.dTag(),
|
||||||
"https://i.postimg.cc/GtDgGY5v/5062563795762785335.jpg",
|
media = followCard.image()?.ifBlank { null },
|
||||||
"Desc",
|
description = followCard.description(),
|
||||||
persistentListOf(
|
users = followCard.followIds().toImmutableList(),
|
||||||
accountViewModel.userProfile(),
|
)
|
||||||
accountViewModel.userProfile(),
|
|
||||||
accountViewModel.userProfile(),
|
ThemeComparisonColumn {
|
||||||
accountViewModel.userProfile(),
|
RenderFollowSetThumb(
|
||||||
accountViewModel.userProfile(),
|
card = card,
|
||||||
),
|
baseNote = LocalCache.getOrCreateNote(followCard.id),
|
||||||
),
|
|
||||||
baseNote = Note(""),
|
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -164,7 +182,7 @@ fun RenderFollowSetThumb(
|
|||||||
)
|
)
|
||||||
} ?: run { DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier) }
|
} ?: run { DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier) }
|
||||||
|
|
||||||
Gallery(card.users, Modifier.padding(Size10dp), accountViewModel, nav)
|
GalleryUnloaded(card.users, StdPadding, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = DoubleVertSpacer)
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|||||||
+3
-13
@@ -45,7 +45,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
@@ -81,11 +80,9 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
||||||
@@ -139,7 +136,7 @@ fun NewProductScreen(
|
|||||||
WatchAndLoadMyEmojiList(accountViewModel)
|
WatchAndLoadMyEmojiList(accountViewModel)
|
||||||
|
|
||||||
BackHandler {
|
BackHandler {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendDraftSync()
|
postViewModel.sendDraftSync()
|
||||||
postViewModel.cancel()
|
postViewModel.cancel()
|
||||||
}
|
}
|
||||||
@@ -154,24 +151,17 @@ fun NewProductScreen(
|
|||||||
onCancel = {
|
onCancel = {
|
||||||
// uses the accountViewModel scope to avoid cancelling this
|
// uses the accountViewModel scope to avoid cancelling this
|
||||||
// function when the postViewModel is released
|
// function when the postViewModel is released
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendDraftSync()
|
postViewModel.sendDraftSync()
|
||||||
postViewModel.cancel()
|
postViewModel.cancel()
|
||||||
}
|
}
|
||||||
nav.popBack()
|
nav.popBack()
|
||||||
},
|
},
|
||||||
onPost = {
|
onPost = {
|
||||||
try {
|
accountViewModel.launchSigner {
|
||||||
accountViewModel.viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
postViewModel.sendPostSync()
|
postViewModel.sendPostSync()
|
||||||
nav.popBack()
|
nav.popBack()
|
||||||
}
|
}
|
||||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
|
||||||
accountViewModel.toastManager.toast(
|
|
||||||
R.string.read_only_user,
|
|
||||||
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-5
@@ -104,15 +104,15 @@ open class NewProductViewModel :
|
|||||||
IZapRaiser {
|
IZapRaiser {
|
||||||
val draftTag = DraftTagState()
|
val draftTag = DraftTagState()
|
||||||
|
|
||||||
var accountViewModel: AccountViewModel? = null
|
lateinit var accountViewModel: AccountViewModel
|
||||||
var account: Account? = null
|
lateinit var account: Account
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
draftTag.versions.collectLatest {
|
draftTag.versions.collectLatest {
|
||||||
// don't save the first
|
// don't save the first
|
||||||
if (it > 0) {
|
if (it > 0) {
|
||||||
accountViewModel?.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
sendDraftSync()
|
sendDraftSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,8 +189,6 @@ open class NewProductViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun editFromDraft(draft: Note) {
|
fun editFromDraft(draft: Note) {
|
||||||
val accountViewModel = accountViewModel ?: return
|
|
||||||
|
|
||||||
val noteEvent = draft.event
|
val noteEvent = draft.event
|
||||||
val noteAuthor = draft.author
|
val noteAuthor = draft.author
|
||||||
|
|
||||||
|
|||||||
+329
@@ -0,0 +1,329 @@
|
|||||||
|
/**
|
||||||
|
* 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.foundation.pager.PagerState
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 = {
|
||||||
|
FollowPackFeedTopBar(
|
||||||
|
note,
|
||||||
|
pagerState,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun FollowPackFeedTopBar(
|
||||||
|
note: AddressableNote,
|
||||||
|
pagerState: PagerState,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
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) } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
}
|
||||||
+1
-1
@@ -57,7 +57,7 @@ fun AddOutboxRelayCardPreview() {
|
|||||||
ThemeComparisonColumn {
|
ThemeComparisonColumn {
|
||||||
AddInboxRelayCard(
|
AddInboxRelayCard(
|
||||||
accountViewModel = mockAccountViewModel(),
|
accountViewModel = mockAccountViewModel(),
|
||||||
nav = EmptyNav,
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@ private fun TopNavFilterBar(
|
|||||||
placeholderCode = listName,
|
placeholderCode = listName,
|
||||||
explainer = stringRes(R.string.select_list_to_filter),
|
explainer = stringRes(R.string.select_list_to_filter),
|
||||||
options = allLists,
|
options = allLists,
|
||||||
onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow) },
|
onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.allFollows) },
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -167,7 +167,7 @@ private fun NewPostScreenInner(
|
|||||||
WatchAndLoadMyEmojiList(accountViewModel)
|
WatchAndLoadMyEmojiList(accountViewModel)
|
||||||
|
|
||||||
BackHandler {
|
BackHandler {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendDraftSync()
|
postViewModel.sendDraftSync()
|
||||||
postViewModel.cancel()
|
postViewModel.cancel()
|
||||||
}
|
}
|
||||||
@@ -181,7 +181,7 @@ private fun NewPostScreenInner(
|
|||||||
onPost = {
|
onPost = {
|
||||||
// uses the accountViewModel scope to avoid cancelling this
|
// uses the accountViewModel scope to avoid cancelling this
|
||||||
// function when the postViewModel is released
|
// function when the postViewModel is released
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendPostSync()
|
postViewModel.sendPostSync()
|
||||||
nav.popBack()
|
nav.popBack()
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ private fun NewPostScreenInner(
|
|||||||
onCancel = {
|
onCancel = {
|
||||||
// uses the accountViewModel scope to avoid cancelling this
|
// uses the accountViewModel scope to avoid cancelling this
|
||||||
// function when the postViewModel is released
|
// function when the postViewModel is released
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
postViewModel.sendDraftSync()
|
postViewModel.sendDraftSync()
|
||||||
postViewModel.cancel()
|
postViewModel.cancel()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -146,7 +146,7 @@ open class ShortNotePostViewModel :
|
|||||||
draftTag.versions.collectLatest {
|
draftTag.versions.collectLatest {
|
||||||
// don't save the first
|
// don't save the first
|
||||||
if (it > 0) {
|
if (it > 0) {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
sendDraftSync()
|
sendDraftSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -483,7 +483,7 @@ open class ShortNotePostViewModel :
|
|||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast)
|
accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast)
|
||||||
accountViewModel.viewModelScope.launch(Dispatchers.IO) {
|
accountViewModel.launchSigner {
|
||||||
accountViewModel.account.deleteDraftIgnoreErrors(version)
|
accountViewModel.account.deleteDraftIgnoreErrors(version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+281
@@ -0,0 +1,281 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.display
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults.cardElevation
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.AboutDisplay
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.AnimateOnNewSearch
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.LightRedColor
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.PopupUpEffect
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderAddUserFieldAndSuggestions(
|
||||||
|
userSuggestions: UserSuggestionState,
|
||||||
|
hasUserFlow: (User) -> Flow<Boolean>,
|
||||||
|
addUserToSet: (User) -> Unit,
|
||||||
|
removeUserFromSet: (User) -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
UserSearchDataSourceSubscription(userSuggestions, accountViewModel)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.newEventBundles.collect {
|
||||||
|
userSuggestions.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
LocalCache.live.deletedEventBundles.collect {
|
||||||
|
userSuggestions.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(HalfVertSpacer)
|
||||||
|
|
||||||
|
var userName by remember(userSuggestions) { mutableStateOf(TextFieldValue(userSuggestions.currentWord.value)) }
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.search_and_add_a_user)) },
|
||||||
|
modifier = Modifier.padding(horizontal = Size10dp).fillMaxWidth(),
|
||||||
|
value = userName,
|
||||||
|
onValueChange = {
|
||||||
|
userName = it
|
||||||
|
userSuggestions.processCurrentWord(it.text)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
userName = TextFieldValue("")
|
||||||
|
userSuggestions.processCurrentWord("")
|
||||||
|
focusManager.clearFocus()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
ClearTextIcon()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ShowUserSuggestions(
|
||||||
|
userSuggestions = userSuggestions,
|
||||||
|
hasUserFlow = hasUserFlow,
|
||||||
|
onSelect = { user ->
|
||||||
|
addUserToSet(user)
|
||||||
|
userName =
|
||||||
|
userName.copy(
|
||||||
|
selection = TextRange(0, userName.text.length),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDelete = { user ->
|
||||||
|
removeUserFromSet(user)
|
||||||
|
userName =
|
||||||
|
userName.copy(
|
||||||
|
selection = TextRange(0, userName.text.length),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShowUserSuggestions(
|
||||||
|
userSuggestions: UserSuggestionState,
|
||||||
|
hasUserFlow: (User) -> Flow<Boolean>,
|
||||||
|
onSelect: (User) -> Unit,
|
||||||
|
onDelete: (User) -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
AnimateOnNewSearch(userSuggestions, listState)
|
||||||
|
|
||||||
|
val suggestions by userSuggestions.results.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
|
if (suggestions.isNotEmpty()) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.padding(start = 11.dp, end = 11.dp),
|
||||||
|
elevation = cardElevation(5.dp),
|
||||||
|
shape = PopupUpEffect,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(top = 10.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.heightIn(0.dp, 200.dp),
|
||||||
|
state = listState,
|
||||||
|
) {
|
||||||
|
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, baseUser ->
|
||||||
|
DrawUser(baseUser, hasUserFlow, onSelect, onDelete, accountViewModel)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = DividerThickness,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(StdVertSpacer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DrawUser(
|
||||||
|
baseUser: User,
|
||||||
|
hasUserFlow: (User) -> Flow<Boolean>,
|
||||||
|
onSelect: (User) -> Unit,
|
||||||
|
onDelete: (User) -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = { onSelect(baseUser) })
|
||||||
|
.padding(
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp,
|
||||||
|
top = 10.dp,
|
||||||
|
bottom = 10.dp,
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
ClickableUserPicture(baseUser, 55.dp, accountViewModel, Modifier, null)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
UsernameDisplay(
|
||||||
|
baseUser,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
HasUserTag(baseUser, hasUserFlow, onDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
AboutDisplay(baseUser, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.HasUserTag(
|
||||||
|
baseUser: User,
|
||||||
|
hasUserFlow: (User) -> Flow<Boolean>,
|
||||||
|
onDelete: (User) -> Unit,
|
||||||
|
) {
|
||||||
|
val hasUserState by hasUserFlow(baseUser).collectAsStateWithLifecycle(false)
|
||||||
|
if (hasUserState) {
|
||||||
|
Spacer(StdHorzSpacer)
|
||||||
|
Text(
|
||||||
|
text = stringRes(id = R.string.in_the_list),
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier =
|
||||||
|
remember {
|
||||||
|
Modifier
|
||||||
|
.clip(SmallBorder)
|
||||||
|
.background(Color.Black)
|
||||||
|
.padding(horizontal = 5.dp)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(30.dp).padding(start = 10.dp),
|
||||||
|
onClick = { onDelete(baseUser) },
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Cancel,
|
||||||
|
contentDescription = stringRes(id = R.string.remove),
|
||||||
|
modifier = Modifier.size(15.dp),
|
||||||
|
tint = LightRedColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+124
-267
@@ -18,16 +18,13 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -38,12 +35,9 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Cancel
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults.cardElevation
|
import androidx.compose.material3.CardDefaults.cardElevation
|
||||||
@@ -51,7 +45,6 @@ import androidx.compose.material3.DropdownMenu
|
|||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -62,60 +55,48 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.TextRange
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.AboutDisplay
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.AnimateOnNewSearch
|
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.DrawUser
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.LightRedColor
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.PopupUpEffect
|
import com.vitorpamplona.amethyst.ui.theme.PopupUpEffect
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -151,24 +132,7 @@ fun PeopleListScreen(
|
|||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
TabRow(
|
TopAppTabs(viewModel, pagerState)
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
|
||||||
selectedTabIndex = pagerState.currentPage,
|
|
||||||
modifier = TabRowHeight,
|
|
||||||
) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
Tab(
|
|
||||||
selected = pagerState.currentPage == 0,
|
|
||||||
onClick = { scope.launch { pagerState.animateScrollToPage(0) } },
|
|
||||||
text = { Text(text = stringRes(R.string.private_members)) },
|
|
||||||
)
|
|
||||||
Tab(
|
|
||||||
selected = pagerState.currentPage == 1,
|
|
||||||
onClick = { scope.launch { pagerState.animateScrollToPage(1) } },
|
|
||||||
text = { Text(text = stringRes(R.string.public_members)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
@@ -190,7 +154,46 @@ fun PeopleListScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TitleAndDescription(viewModel: PeopleListViewModel) {
|
private fun TopAppTabs(
|
||||||
|
viewModel: PeopleListViewModel,
|
||||||
|
pagerState: PagerState,
|
||||||
|
) {
|
||||||
|
TabRow(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
modifier = TabRowHeight,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 0,
|
||||||
|
onClick = { scope.launch { pagerState.animateScrollToPage(0) } },
|
||||||
|
text = {
|
||||||
|
val list = viewModel.selectedList.collectAsStateWithLifecycle()
|
||||||
|
val labelPublic =
|
||||||
|
list.value?.let {
|
||||||
|
stringRes(R.string.public_members_count, it.publicMembers.size)
|
||||||
|
} ?: stringRes(R.string.public_members)
|
||||||
|
Text(labelPublic)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 1,
|
||||||
|
onClick = { scope.launch { pagerState.animateScrollToPage(1) } },
|
||||||
|
text = {
|
||||||
|
val list = viewModel.selectedList.collectAsStateWithLifecycle()
|
||||||
|
val labelPrivate =
|
||||||
|
list.value?.let {
|
||||||
|
stringRes(R.string.private_members_count, it.privateMembersList.size)
|
||||||
|
} ?: stringRes(R.string.private_members)
|
||||||
|
Text(labelPrivate)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TitleAndDescription(viewModel: PeopleListViewModel) {
|
||||||
val selectedSetState = viewModel.selectedList.collectAsStateWithLifecycle()
|
val selectedSetState = viewModel.selectedList.collectAsStateWithLifecycle()
|
||||||
selectedSetState.value?.let { selectedSet ->
|
selectedSetState.value?.let { selectedSet ->
|
||||||
Text(
|
Text(
|
||||||
@@ -215,7 +218,7 @@ private fun ListViewAndEditColumn(
|
|||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
onDeleteUser = { user, isPrivate ->
|
onDeleteUser = { user, isPrivate ->
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
viewModel.removeUserFromSet(user, isPrivate)
|
viewModel.removeUserFromSet(user, isPrivate)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -233,70 +236,20 @@ private fun RenderAddUserFieldAndSuggestions(
|
|||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
UserSearchDataSourceSubscription(viewModel.userSuggestions, accountViewModel)
|
RenderAddUserFieldAndSuggestions(
|
||||||
|
viewModel.userSuggestions,
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
launch(Dispatchers.IO) {
|
|
||||||
LocalCache.live.newEventBundles.collect {
|
|
||||||
viewModel.userSuggestions.invalidateData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch(Dispatchers.IO) {
|
|
||||||
LocalCache.live.deletedEventBundles.collect {
|
|
||||||
viewModel.userSuggestions.invalidateData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(HalfVertSpacer)
|
|
||||||
|
|
||||||
var userName by remember(viewModel) { mutableStateOf(TextFieldValue(viewModel.userSuggestions.currentWord.value)) }
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
label = { Text(text = stringRes(R.string.search_and_add_a_user)) },
|
|
||||||
modifier = Modifier.padding(horizontal = Size10dp).fillMaxWidth(),
|
|
||||||
value = userName,
|
|
||||||
onValueChange = {
|
|
||||||
userName = it
|
|
||||||
viewModel.userSuggestions.processCurrentWord(it.text)
|
|
||||||
},
|
|
||||||
singleLine = true,
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
userName = TextFieldValue("")
|
|
||||||
viewModel.userSuggestions.processCurrentWord("")
|
|
||||||
focusManager.clearFocus()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
ClearTextIcon()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
ShowUserSuggestions(
|
|
||||||
userSuggestions = viewModel.userSuggestions,
|
|
||||||
hasUserFlow = { user ->
|
hasUserFlow = { user ->
|
||||||
viewModel.hasUserFlow(user, pagerState.currentPage == 0)
|
viewModel.hasUserFlow(user, pagerState.currentPage == 1)
|
||||||
},
|
},
|
||||||
onSelect = { user ->
|
addUserToSet = { user ->
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
viewModel.addUserToSet(user, pagerState.currentPage == 0)
|
viewModel.addUserToSet(user, pagerState.currentPage == 1)
|
||||||
}
|
}
|
||||||
userName =
|
|
||||||
userName.copy(
|
|
||||||
selection = TextRange(0, userName.text.length),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onDelete = { user ->
|
removeUserFromSet = { user ->
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
viewModel.removeUserFromSet(user, pagerState.currentPage == 0)
|
viewModel.removeUserFromSet(user, pagerState.currentPage == 1)
|
||||||
}
|
}
|
||||||
userName =
|
|
||||||
userName.copy(
|
|
||||||
selection = TextRange(0, userName.text.length),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
)
|
)
|
||||||
@@ -317,9 +270,9 @@ private fun PeopleListPager(
|
|||||||
when (page) {
|
when (page) {
|
||||||
0 ->
|
0 ->
|
||||||
PeopleListView(
|
PeopleListView(
|
||||||
memberList = selectedSet.privateMembersList,
|
memberList = selectedSet.publicMembersList,
|
||||||
onDeleteUser = { user ->
|
onDeleteUser = { user ->
|
||||||
onDeleteUser(user, true)
|
onDeleteUser(user, false)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
@@ -328,9 +281,9 @@ private fun PeopleListPager(
|
|||||||
|
|
||||||
1 ->
|
1 ->
|
||||||
PeopleListView(
|
PeopleListView(
|
||||||
memberList = selectedSet.publicMembersList,
|
memberList = selectedSet.privateMembersList,
|
||||||
onDeleteUser = { user ->
|
onDeleteUser = { user ->
|
||||||
onDeleteUser(user, false)
|
onDeleteUser(user, true)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
@@ -343,7 +296,7 @@ private fun PeopleListPager(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview(device = "spec:width=2160px,height=2940px,dpi=440")
|
@Preview(device = "spec:width=2160px,height=2940px,dpi=440")
|
||||||
fun FollowSetListViewPreview() {
|
private fun PeopleListViewPreview() {
|
||||||
val accountViewModel = mockAccountViewModel()
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||||
@@ -356,7 +309,7 @@ fun FollowSetListViewPreview() {
|
|||||||
memberList = persistentListOf(user1, user2, user3),
|
memberList = persistentListOf(user1, user2, user3),
|
||||||
onDeleteUser = { user -> },
|
onDeleteUser = { user -> },
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = EmptyNav,
|
nav = EmptyNav(),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(HalfVertSpacer)
|
Spacer(HalfVertSpacer)
|
||||||
@@ -411,144 +364,25 @@ fun FollowSetListViewPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShowUserSuggestions(
|
private fun ListActionsMenuButton(
|
||||||
userSuggestions: UserSuggestionState,
|
|
||||||
hasUserFlow: (User) -> Flow<Boolean>,
|
|
||||||
onSelect: (User) -> Unit,
|
|
||||||
onDelete: (User) -> Unit,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
|
|
||||||
AnimateOnNewSearch(userSuggestions, listState)
|
|
||||||
|
|
||||||
val suggestions by userSuggestions.results.collectAsStateWithLifecycle(emptyList())
|
|
||||||
|
|
||||||
if (suggestions.isNotEmpty()) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.padding(start = 11.dp, end = 11.dp),
|
|
||||||
elevation = cardElevation(5.dp),
|
|
||||||
shape = PopupUpEffect,
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
contentPadding = PaddingValues(top = 10.dp),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.heightIn(0.dp, 200.dp),
|
|
||||||
state = listState,
|
|
||||||
) {
|
|
||||||
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, baseUser ->
|
|
||||||
DrawUser(baseUser, hasUserFlow, onSelect, onDelete, accountViewModel)
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = DividerThickness,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(StdVertSpacer)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DrawUser(
|
|
||||||
baseUser: User,
|
|
||||||
hasUserFlow: (User) -> Flow<Boolean>,
|
|
||||||
onSelect: (User) -> Unit,
|
|
||||||
onDelete: (User) -> Unit,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = { onSelect(baseUser) })
|
|
||||||
.padding(
|
|
||||||
start = 12.dp,
|
|
||||||
end = 12.dp,
|
|
||||||
top = 10.dp,
|
|
||||||
bottom = 10.dp,
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
ClickableUserPicture(baseUser, 55.dp, accountViewModel, Modifier, null)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(start = 10.dp)
|
|
||||||
.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
UsernameDisplay(
|
|
||||||
baseUser,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
)
|
|
||||||
HasUserTag(baseUser, hasUserFlow, onDelete)
|
|
||||||
}
|
|
||||||
|
|
||||||
AboutDisplay(baseUser, accountViewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RowScope.HasUserTag(
|
|
||||||
baseUser: User,
|
|
||||||
hasUserFlow: (User) -> Flow<Boolean>,
|
|
||||||
onDelete: (User) -> Unit,
|
|
||||||
) {
|
|
||||||
val hasUserState by hasUserFlow(baseUser).collectAsStateWithLifecycle(false)
|
|
||||||
if (hasUserState) {
|
|
||||||
Spacer(StdHorzSpacer)
|
|
||||||
Text(
|
|
||||||
text = stringRes(id = R.string.in_the_list),
|
|
||||||
color = Color.White,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier =
|
|
||||||
remember {
|
|
||||||
Modifier
|
|
||||||
.clip(SmallBorder)
|
|
||||||
.background(Color.Black)
|
|
||||||
.padding(horizontal = 5.dp)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.size(30.dp).padding(start = 10.dp),
|
|
||||||
onClick = { onDelete(baseUser) },
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Cancel,
|
|
||||||
contentDescription = stringRes(id = R.string.remove),
|
|
||||||
modifier = Modifier.size(15.dp),
|
|
||||||
tint = LightRedColor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ListActionsMenuButton(
|
|
||||||
viewModel: PeopleListViewModel,
|
viewModel: PeopleListViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
ListActionsMenuButton(
|
ListActionsMenuButton(
|
||||||
|
note = viewModel::selectedNote,
|
||||||
|
onEditList = {
|
||||||
|
nav.nav { Route.PeopleListMetadataEdit(viewModel.selectedDTag.value) }
|
||||||
|
},
|
||||||
onBroadcastList = {
|
onBroadcastList = {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
viewModel.loadNote()?.let { updatedSetNote ->
|
viewModel.loadNote()?.let { updatedSetNote ->
|
||||||
accountViewModel.broadcast(updatedSetNote)
|
accountViewModel.broadcast(updatedSetNote)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDeleteList = {
|
onDeleteList = {
|
||||||
accountViewModel.runIOCatching {
|
accountViewModel.launchSigner {
|
||||||
viewModel.deleteFollowSet()
|
viewModel.deleteFollowSet()
|
||||||
}
|
}
|
||||||
nav.popBack()
|
nav.popBack()
|
||||||
@@ -557,7 +391,9 @@ fun ListActionsMenuButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListActionsMenuButton(
|
private fun ListActionsMenuButton(
|
||||||
|
note: () -> AddressableNote,
|
||||||
|
onEditList: () -> Unit,
|
||||||
onBroadcastList: () -> Unit,
|
onBroadcastList: () -> Unit,
|
||||||
onDeleteList: () -> Unit,
|
onDeleteList: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -578,44 +414,65 @@ fun ListActionsMenuButton(
|
|||||||
onClick = { isActionListOpen.value = true },
|
onClick = { isActionListOpen.value = true },
|
||||||
) {
|
) {
|
||||||
VerticalDotsIcon()
|
VerticalDotsIcon()
|
||||||
ListActionsMenu(
|
|
||||||
onCloseMenu = { isActionListOpen.value = false },
|
DropdownMenu(
|
||||||
isOpen = isActionListOpen.value,
|
expanded = isActionListOpen.value,
|
||||||
onBroadcastList = onBroadcastList,
|
onDismissRequest = { isActionListOpen.value = false },
|
||||||
onDeleteList = onDeleteList,
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringRes(R.string.quick_action_share)) },
|
||||||
|
onClick = {
|
||||||
|
val sendIntent =
|
||||||
|
Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
externalLinkForNote(note()),
|
||||||
|
)
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TITLE,
|
||||||
|
stringRes(context, R.string.quick_action_share_browser_link),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
val shareIntent =
|
||||||
fun ListActionsMenu(
|
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
||||||
onCloseMenu: () -> Unit,
|
ContextCompat.startActivity(context, shareIntent, null)
|
||||||
isOpen: Boolean,
|
isActionListOpen.value = false
|
||||||
onBroadcastList: () -> Unit,
|
|
||||||
onDeleteList: () -> Unit,
|
|
||||||
) {
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = isOpen,
|
|
||||||
onDismissRequest = onCloseMenu,
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text("Broadcast List")
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
onBroadcastList()
|
|
||||||
onCloseMenu()
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text("Delete List")
|
Text(stringRes(R.string.follow_set_edit_list_metadata))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onEditList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_set_broadcast))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onBroadcastList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_set_delete))
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
onDeleteList()
|
onDeleteList()
|
||||||
onCloseMenu()
|
isActionListOpen.value = false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+6
-1
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -60,6 +61,10 @@ class PeopleListViewModel : ViewModel() {
|
|||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||||
|
|
||||||
|
fun selectedAddress() = PeopleListEvent.createAddress(account.userProfile().pubkeyHex, selectedDTag.value)
|
||||||
|
|
||||||
|
fun selectedNote() = account.cache.getOrCreateAddressableNote(selectedAddress())
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
account: Account,
|
account: Account,
|
||||||
selectedDTag: String,
|
selectedDTag: String,
|
||||||
+404
@@ -0,0 +1,404 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.display.packs
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults.cardElevation
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.DrawUser
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.PopupUpEffect
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FollowPackScreen(
|
||||||
|
selectedDTag: String,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val viewModel: FollowPackViewModel = viewModel()
|
||||||
|
viewModel.init(accountViewModel.account, selectedDTag)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
Column {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
TitleAndDescription(viewModel)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(nav::popBack) {
|
||||||
|
ArrowBackIcon()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
ListActionsMenuButton(viewModel, accountViewModel, nav)
|
||||||
|
},
|
||||||
|
colors =
|
||||||
|
TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
ListViewAndEditColumn(
|
||||||
|
viewModel = viewModel,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(
|
||||||
|
top = padding.calculateTopPadding(),
|
||||||
|
bottom = padding.calculateBottomPadding(),
|
||||||
|
).consumeWindowInsets(padding)
|
||||||
|
.imePadding(),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TitleAndDescription(viewModel: FollowPackViewModel) {
|
||||||
|
val selectedSetState = viewModel.selectedList.collectAsStateWithLifecycle()
|
||||||
|
selectedSetState.value?.let { selectedSet ->
|
||||||
|
Text(
|
||||||
|
text = selectedSet.title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListViewAndEditColumn(
|
||||||
|
viewModel: FollowPackViewModel,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
PeopleListPager(
|
||||||
|
viewModel = viewModel,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onDeleteUser = { user ->
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
viewModel.removeUserFromSet(user)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
|
||||||
|
RenderAddUserFieldAndSuggestions(viewModel, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderAddUserFieldAndSuggestions(
|
||||||
|
viewModel: FollowPackViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
RenderAddUserFieldAndSuggestions(
|
||||||
|
viewModel.userSuggestions,
|
||||||
|
hasUserFlow = { user ->
|
||||||
|
viewModel.hasUserFlow(user)
|
||||||
|
},
|
||||||
|
addUserToSet = { user ->
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
viewModel.addUserToSet(user)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeUserFromSet = { user ->
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
viewModel.removeUserFromSet(user)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PeopleListPager(
|
||||||
|
viewModel: FollowPackViewModel,
|
||||||
|
modifier: Modifier,
|
||||||
|
onDeleteUser: (User) -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val selectedSetState = viewModel.selectedList.collectAsStateWithLifecycle()
|
||||||
|
selectedSetState.value?.let { selectedSet ->
|
||||||
|
PeopleListView(
|
||||||
|
memberList = selectedSet.publicMembersList,
|
||||||
|
onDeleteUser = onDeleteUser,
|
||||||
|
modifier = modifier,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListActionsMenuButton(
|
||||||
|
viewModel: FollowPackViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
ListActionsMenuButton(
|
||||||
|
note = viewModel::selectedNote,
|
||||||
|
onEditList = {
|
||||||
|
nav.nav { Route.FollowPackMetadataEdit(viewModel.selectedDTag.value) }
|
||||||
|
},
|
||||||
|
onBroadcastList = {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
viewModel.loadNote()?.let { updatedSetNote ->
|
||||||
|
accountViewModel.broadcast(updatedSetNote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDeleteList = {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
viewModel.deleteFollowSet()
|
||||||
|
}
|
||||||
|
nav.popBack()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListActionsMenuButton(
|
||||||
|
note: () -> AddressableNote,
|
||||||
|
onEditList: () -> Unit,
|
||||||
|
onBroadcastList: () -> Unit,
|
||||||
|
onDeleteList: () -> Unit,
|
||||||
|
) {
|
||||||
|
val isActionListOpen = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ClickableBox(
|
||||||
|
modifier =
|
||||||
|
StdPadding
|
||||||
|
.size(30.dp)
|
||||||
|
.border(
|
||||||
|
width = Dp.Hairline,
|
||||||
|
color = ButtonDefaults.filledTonalButtonColors().containerColor,
|
||||||
|
shape = ButtonBorder,
|
||||||
|
).background(
|
||||||
|
color = ButtonDefaults.filledTonalButtonColors().containerColor,
|
||||||
|
shape = ButtonBorder,
|
||||||
|
),
|
||||||
|
onClick = { isActionListOpen.value = true },
|
||||||
|
) {
|
||||||
|
VerticalDotsIcon()
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = isActionListOpen.value,
|
||||||
|
onDismissRequest = { isActionListOpen.value = false },
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringRes(R.string.quick_action_share)) },
|
||||||
|
onClick = {
|
||||||
|
val sendIntent =
|
||||||
|
Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
externalLinkForNote(note()),
|
||||||
|
)
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TITLE,
|
||||||
|
stringRes(context, R.string.quick_action_share_browser_link),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shareIntent =
|
||||||
|
Intent.createChooser(sendIntent, stringRes(context, R.string.quick_action_share))
|
||||||
|
ContextCompat.startActivity(context, shareIntent, null)
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_pack_edit_list_metadata))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onEditList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_pack_broadcast))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onBroadcastList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(stringRes(R.string.follow_pack_delete))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onDeleteList()
|
||||||
|
isActionListOpen.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview(device = "spec:width=2160px,height=2940px,dpi=440")
|
||||||
|
fun FollowPackViewPreview() {
|
||||||
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
|
||||||
|
val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||||
|
val user2: User = LocalCache.getOrCreateUser("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b")
|
||||||
|
val user3: User = LocalCache.getOrCreateUser("7eb29c126b3628077e2e3d863b917a56b74293aa9d8a9abc26a40ba3f2866baf")
|
||||||
|
|
||||||
|
ThemeComparisonRow {
|
||||||
|
Column {
|
||||||
|
PeopleListView(
|
||||||
|
memberList = persistentListOf(user1, user2, user3),
|
||||||
|
onDeleteUser = { user -> },
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = EmptyNav(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(HalfVertSpacer)
|
||||||
|
|
||||||
|
var userName by remember { mutableStateOf("") }
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.search_and_add_a_user)) },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = Size10dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
value = userName,
|
||||||
|
onValueChange = {
|
||||||
|
userName = it
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
ClearTextIcon()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp),
|
||||||
|
elevation = cardElevation(5.dp),
|
||||||
|
shape = PopupUpEffect,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(top = 10.dp),
|
||||||
|
modifier = Modifier.heightIn(0.dp, 200.dp),
|
||||||
|
) {
|
||||||
|
itemsIndexed(persistentListOf(user1, user2, user3), key = { _, item -> item.pubkeyHex }) { _, baseUser ->
|
||||||
|
DrawUser(
|
||||||
|
baseUser,
|
||||||
|
{ MutableStateFlow(false) },
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
accountViewModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = DividerThickness,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.display.packs
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class FollowPackViewModel : ViewModel() {
|
||||||
|
lateinit var account: Account
|
||||||
|
lateinit var userSuggestions: UserSuggestionState
|
||||||
|
|
||||||
|
var userSuggestionFocus by mutableStateOf<UserSuggestionState?>(null)
|
||||||
|
|
||||||
|
val selectedDTag = MutableStateFlow("")
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val selectedList =
|
||||||
|
selectedDTag
|
||||||
|
.transformLatest {
|
||||||
|
emitAll(
|
||||||
|
account.followLists.selectListFlow(it).flowOn(Dispatchers.IO),
|
||||||
|
)
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||||
|
|
||||||
|
fun selectedAddress() = FollowListEvent.createAddress(account.userProfile().pubkeyHex, selectedDTag.value)
|
||||||
|
|
||||||
|
fun selectedNote() = account.cache.getOrCreateAddressableNote(selectedAddress())
|
||||||
|
|
||||||
|
fun init(
|
||||||
|
account: Account,
|
||||||
|
selectedDTag: String,
|
||||||
|
) {
|
||||||
|
if (!this::account.isInitialized || this.account != account) {
|
||||||
|
this.account = account
|
||||||
|
this.userSuggestions = UserSuggestionState(account, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedDTag.tryEmit(selectedDTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteFollowSet() {
|
||||||
|
account.followLists.deleteFollowSet(selectedDTag.value, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadNote(): AddressableNote? = account.followLists.getPeopleListNote(selectedDTag.value)
|
||||||
|
|
||||||
|
suspend fun removeUserFromSet(user: User) {
|
||||||
|
account.followLists.removeUserFromSet(user, selectedDTag.value, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun addUserToSet(user: User) {
|
||||||
|
account.followLists.addUserToSet(user, selectedDTag.value, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasUserFlow(user: User): Flow<Boolean> =
|
||||||
|
selectedList.map {
|
||||||
|
if (it == null) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
user in it.publicMembers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.list
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class FollowPackViewModel : ViewModel() {
|
||||||
|
lateinit var accountViewModel: AccountViewModel
|
||||||
|
|
||||||
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
|
this.accountViewModel = accountViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listFlow() = accountViewModel.account.followLists.uiListFlow
|
||||||
|
|
||||||
|
fun cloneItem(
|
||||||
|
followSet: PeopleList,
|
||||||
|
customName: String?,
|
||||||
|
customDescription: String?,
|
||||||
|
) {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
accountViewModel.account.followLists.cloneFollowSet(
|
||||||
|
currentFollowPack = followSet,
|
||||||
|
customCloneName = customName,
|
||||||
|
customCloneDescription = customDescription,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteItem(followSet: PeopleList) {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
accountViewModel.account.followLists.deleteFollowSet(
|
||||||
|
identifierTag = followSet.identifierTag,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
-19
@@ -22,40 +22,45 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AllPeopleListFeedView(
|
fun AllPeopleListFeedView(
|
||||||
listFlow: StateFlow<List<PeopleList>>,
|
peopleListModel: PeopleListViewModel,
|
||||||
onOpenItem: (String) -> Unit = {},
|
followPackModel: FollowPackViewModel,
|
||||||
onRenameItem: (targetSet: PeopleList, newName: String) -> Unit,
|
nav: INav,
|
||||||
onItemDescriptionChange: (peopleList: PeopleList, newDescription: String?) -> Unit,
|
|
||||||
onItemClone: (peopleList: PeopleList, customName: String?, customDesc: String?) -> Unit,
|
|
||||||
onDeleteItem: (peopleList: PeopleList) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val followSetFeedState by listFlow.collectAsStateWithLifecycle()
|
val peopleListFeedState by peopleListModel.listFlow().collectAsStateWithLifecycle()
|
||||||
|
val followPackFeedState by followPackModel.listFlow().collectAsStateWithLifecycle()
|
||||||
|
|
||||||
if (followSetFeedState.isEmpty()) {
|
if (peopleListFeedState.isEmpty() && followPackFeedState.isEmpty()) {
|
||||||
AllPeopleListFeedEmpty(
|
AllPeopleListFeedEmpty(
|
||||||
message = stringRes(R.string.follow_set_empty_feed_msg),
|
message = stringRes(R.string.follow_set_empty_feed_msg),
|
||||||
)
|
)
|
||||||
@@ -64,15 +69,84 @@ fun AllPeopleListFeedView(
|
|||||||
state = rememberLazyListState(),
|
state = rememberLazyListState(),
|
||||||
contentPadding = FeedPadding,
|
contentPadding = FeedPadding,
|
||||||
) {
|
) {
|
||||||
itemsIndexed(followSetFeedState, key = { _, item -> item.identifierTag }) { _, list ->
|
stickyHeader {
|
||||||
|
Row(
|
||||||
|
modifier = MaxWidthWithHorzPadding,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = SpacedBy5dp,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.follow_sets),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.follow_sets_explainer),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
NewListButton(
|
||||||
|
onClick = {
|
||||||
|
nav.nav(Route.PeopleListMetadataEdit())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemsIndexed(peopleListFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
||||||
PeopleListItem(
|
PeopleListItem(
|
||||||
modifier = Modifier.fillMaxSize().animateItem(),
|
modifier =
|
||||||
peopleList = list,
|
Modifier
|
||||||
onClick = { onOpenItem(list.identifierTag) },
|
.fillMaxSize()
|
||||||
onRename = { onRenameItem(list, it) },
|
.animateItem(),
|
||||||
onDescriptionChange = { newDescription -> onItemDescriptionChange(list, newDescription) },
|
peopleList = followSet,
|
||||||
onClone = { cloneName, cloneDescription -> onItemClone(list, cloneName, cloneDescription) },
|
onClick = { nav.nav(Route.MyPeopleListView(followSet.identifierTag)) },
|
||||||
onDelete = { onDeleteItem(list) },
|
onEditMetadata = { nav.nav(Route.PeopleListMetadataEdit(followSet.identifierTag)) },
|
||||||
|
onClone = { cloneName, cloneDescription -> peopleListModel.cloneItem(followSet, cloneName, cloneDescription) },
|
||||||
|
onDelete = { peopleListModel.deleteItem(followSet) },
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
}
|
||||||
|
stickyHeader {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = SpacedBy5dp,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.discover_follows),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.discover_follows_explainer),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
NewListButton(
|
||||||
|
onClick = {
|
||||||
|
nav.nav(Route.FollowPackMetadataEdit())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemsIndexed(followPackFeedState, key = { _, item -> item.identifierTag }) { _, followSet ->
|
||||||
|
PeopleListItem(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.animateItem(),
|
||||||
|
peopleList = followSet,
|
||||||
|
onClick = { nav.nav(Route.MyFollowPackView(followSet.identifierTag)) },
|
||||||
|
onEditMetadata = { nav.nav(Route.FollowPackMetadataEdit(followSet.identifierTag)) },
|
||||||
|
onClone = { cloneName, cloneDescription -> followPackModel.cloneItem(followSet, cloneName, cloneDescription) },
|
||||||
|
onDelete = { followPackModel.deleteItem(followSet) },
|
||||||
)
|
)
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
}
|
}
|
||||||
@@ -83,7 +157,9 @@ fun AllPeopleListFeedView(
|
|||||||
@Composable
|
@Composable
|
||||||
fun AllPeopleListFeedEmpty(message: String = stringRes(R.string.feed_is_empty)) {
|
fun AllPeopleListFeedEmpty(message: String = stringRes(R.string.feed_is_empty)) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxSize().padding(horizontal = Size40dp),
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = Size40dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+20
-105
@@ -21,108 +21,50 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
|
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListOfPeopleListsScreen(
|
fun ListOfPeopleListsScreen(
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
ListOfPeopleListsScreen(
|
val list: PeopleListViewModel = viewModel()
|
||||||
listFlow = accountViewModel.account.peopleLists.uiListFlow,
|
list.init(accountViewModel)
|
||||||
addItem = { title: String, description: String? ->
|
|
||||||
accountViewModel.runIOCatching {
|
val pack: FollowPackViewModel = viewModel()
|
||||||
accountViewModel.account.peopleLists.addFollowList(
|
pack.init(accountViewModel)
|
||||||
listName = title,
|
|
||||||
listDescription = description,
|
ListOfPeopleListsScreen(list, pack, nav)
|
||||||
account = accountViewModel.account,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openItem = {
|
|
||||||
nav.nav(Route.PeopleListView(it))
|
|
||||||
},
|
|
||||||
renameItem = { followSet, newValue ->
|
|
||||||
accountViewModel.runIOCatching {
|
|
||||||
accountViewModel.account.peopleLists.renameFollowList(
|
|
||||||
newName = newValue,
|
|
||||||
peopleList = followSet,
|
|
||||||
account = accountViewModel.account,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
changeItemDescription = { followSet, newDescription ->
|
|
||||||
accountViewModel.runIOCatching {
|
|
||||||
accountViewModel.account.peopleLists.modifyFollowSetDescription(
|
|
||||||
newDescription = newDescription,
|
|
||||||
peopleList = followSet,
|
|
||||||
account = accountViewModel.account,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cloneItem = { followSet, customName, customDescription ->
|
|
||||||
accountViewModel.runIOCatching {
|
|
||||||
accountViewModel.account.peopleLists.cloneFollowSet(
|
|
||||||
currentPeopleList = followSet,
|
|
||||||
customCloneName = customName,
|
|
||||||
customCloneDescription = customDescription,
|
|
||||||
account = accountViewModel.account,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteItem = { followSet ->
|
|
||||||
accountViewModel.runIOCatching {
|
|
||||||
accountViewModel.account.peopleLists.deleteFollowSet(
|
|
||||||
identifierTag = followSet.identifierTag,
|
|
||||||
account = accountViewModel.account,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
nav,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListOfPeopleListsScreen(
|
fun ListOfPeopleListsScreen(
|
||||||
listFlow: StateFlow<List<PeopleList>>,
|
list: PeopleListViewModel,
|
||||||
addItem: (title: String, description: String?) -> Unit,
|
pack: FollowPackViewModel,
|
||||||
openItem: (identifier: String) -> Unit,
|
|
||||||
renameItem: (peopleList: PeopleList, newName: String) -> Unit,
|
|
||||||
changeItemDescription: (peopleList: PeopleList, newDescription: String?) -> Unit,
|
|
||||||
cloneItem: (peopleList: PeopleList, customName: String?, customDesc: String?) -> Unit,
|
|
||||||
deleteItem: (peopleList: PeopleList) -> Unit,
|
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBarWithBackButton(stringRes(R.string.my_lists), nav::popBack)
|
TopBarWithBackButton(stringRes(R.string.my_lists), nav::popBack)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
|
||||||
PeopleListFabsAndMenu(
|
|
||||||
onAddSet = addItem,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
@@ -131,47 +73,20 @@ fun ListOfPeopleListsScreen(
|
|||||||
bottom = paddingValues.calculateBottomPadding(),
|
bottom = paddingValues.calculateBottomPadding(),
|
||||||
).fillMaxHeight(),
|
).fillMaxHeight(),
|
||||||
) {
|
) {
|
||||||
AllPeopleListFeedView(
|
AllPeopleListFeedView(list, pack, nav)
|
||||||
listFlow = listFlow,
|
|
||||||
onOpenItem = openItem,
|
|
||||||
onRenameItem = renameItem,
|
|
||||||
onItemDescriptionChange = changeItemDescription,
|
|
||||||
onItemClone = cloneItem,
|
|
||||||
onDeleteItem = deleteItem,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PeopleListFabsAndMenu(onAddSet: (name: String, description: String?) -> Unit) {
|
fun NewListButton(onClick: () -> Unit) {
|
||||||
val isSetAdditionDialogOpen = remember { mutableStateOf(false) }
|
OutlinedButton(onClick = onClick) {
|
||||||
|
Row(horizontalArrangement = SpacedBy5dp, verticalAlignment = Alignment.CenterVertically) {
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
text = {
|
|
||||||
Text(text = stringRes(R.string.follow_set_create_btn_label))
|
|
||||||
},
|
|
||||||
icon = {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
|
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
},
|
Text(stringRes(R.string.follow_set_create_btn_label))
|
||||||
onClick = {
|
}
|
||||||
isSetAdditionDialogOpen.value = true
|
|
||||||
},
|
|
||||||
shape = CircleShape,
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isSetAdditionDialogOpen.value) {
|
|
||||||
NewPeopleListCreationDialog(
|
|
||||||
onDismiss = {
|
|
||||||
isSetAdditionDialogOpen.value = false
|
|
||||||
},
|
|
||||||
onCreateList = { name, description ->
|
|
||||||
onAddSet(name, description)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewPeopleListCreationDialog(
|
fun NewPeopleListCreationDialog(
|
||||||
|
title: Int = R.string.follow_set_creation_dialog_title,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onCreateList: (name: String, description: String?) -> Unit,
|
onCreateList: (name: String, description: String?) -> Unit,
|
||||||
@@ -56,7 +57,7 @@ fun NewPeopleListCreationDialog(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringRes(R.string.follow_set_creation_dialog_title),
|
text = stringRes(title),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+26
-71
@@ -81,8 +81,9 @@ private fun PeopleListItemPreview() {
|
|||||||
val samplePeopleList1 =
|
val samplePeopleList1 =
|
||||||
PeopleList(
|
PeopleList(
|
||||||
identifierTag = "00001-2222",
|
identifierTag = "00001-2222",
|
||||||
title = "Sample List Title",
|
title = "Sample List Title, Very long title, very very very long",
|
||||||
description = "Sample List Description",
|
description = "Sample List Description",
|
||||||
|
image = "http://some.com/image.png",
|
||||||
emptySet(),
|
emptySet(),
|
||||||
emptySet(),
|
emptySet(),
|
||||||
)
|
)
|
||||||
@@ -92,6 +93,7 @@ private fun PeopleListItemPreview() {
|
|||||||
identifierTag = "00001-2223",
|
identifierTag = "00001-2223",
|
||||||
title = "Sample List Title",
|
title = "Sample List Title",
|
||||||
description = "Sample List Description",
|
description = "Sample List Description",
|
||||||
|
image = "http://some.com/image.png",
|
||||||
setOf(user1, user3),
|
setOf(user1, user3),
|
||||||
emptySet(),
|
emptySet(),
|
||||||
)
|
)
|
||||||
@@ -101,6 +103,7 @@ private fun PeopleListItemPreview() {
|
|||||||
identifierTag = "00001-2224",
|
identifierTag = "00001-2224",
|
||||||
title = "Sample List Title",
|
title = "Sample List Title",
|
||||||
description = "Sample List Description",
|
description = "Sample List Description",
|
||||||
|
image = "http://some.com/image.png",
|
||||||
emptySet(),
|
emptySet(),
|
||||||
setOf(user1, user3),
|
setOf(user1, user3),
|
||||||
)
|
)
|
||||||
@@ -110,6 +113,7 @@ private fun PeopleListItemPreview() {
|
|||||||
identifierTag = "00001-2225",
|
identifierTag = "00001-2225",
|
||||||
title = "Sample List Title",
|
title = "Sample List Title",
|
||||||
description = "Sample List Description",
|
description = "Sample List Description",
|
||||||
|
image = "http://some.com/image.png",
|
||||||
setOf(user3),
|
setOf(user3),
|
||||||
setOf(user1, user2, user3),
|
setOf(user1, user2, user3),
|
||||||
)
|
)
|
||||||
@@ -120,8 +124,7 @@ private fun PeopleListItemPreview() {
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
peopleList = samplePeopleList1,
|
peopleList = samplePeopleList1,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onRename = {},
|
onEditMetadata = {},
|
||||||
onDescriptionChange = { },
|
|
||||||
onClone = { newName, newDesc -> },
|
onClone = { newName, newDesc -> },
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
)
|
)
|
||||||
@@ -129,8 +132,7 @@ private fun PeopleListItemPreview() {
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
peopleList = samplePeopleList2,
|
peopleList = samplePeopleList2,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onRename = {},
|
onEditMetadata = {},
|
||||||
onDescriptionChange = { },
|
|
||||||
onClone = { newName, newDesc -> },
|
onClone = { newName, newDesc -> },
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
)
|
)
|
||||||
@@ -138,8 +140,7 @@ private fun PeopleListItemPreview() {
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
peopleList = samplePeopleList3,
|
peopleList = samplePeopleList3,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onRename = {},
|
onEditMetadata = {},
|
||||||
onDescriptionChange = { },
|
|
||||||
onClone = { newName, newDesc -> },
|
onClone = { newName, newDesc -> },
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
)
|
)
|
||||||
@@ -147,8 +148,7 @@ private fun PeopleListItemPreview() {
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
peopleList = samplePeopleList4,
|
peopleList = samplePeopleList4,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onRename = {},
|
onEditMetadata = {},
|
||||||
onDescriptionChange = { },
|
|
||||||
onClone = { newName, newDesc -> },
|
onClone = { newName, newDesc -> },
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
)
|
)
|
||||||
@@ -161,8 +161,7 @@ fun PeopleListItem(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
peopleList: PeopleList,
|
peopleList: PeopleList,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onRename: (String) -> Unit,
|
onEditMetadata: () -> Unit,
|
||||||
onDescriptionChange: (String?) -> Unit,
|
|
||||||
onClone: (customName: String?, customDescription: String?) -> Unit,
|
onClone: (customName: String?, customDescription: String?) -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -173,7 +172,12 @@ fun PeopleListItem(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(peopleList.title, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = peopleList.title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = NoSoTinyBorders,
|
modifier = NoSoTinyBorders,
|
||||||
@@ -181,10 +185,7 @@ fun PeopleListItem(
|
|||||||
horizontalAlignment = Alignment.End,
|
horizontalAlignment = Alignment.End,
|
||||||
) {
|
) {
|
||||||
PeopleListOptionsButton(
|
PeopleListOptionsButton(
|
||||||
peopleListName = peopleList.title,
|
onListEditMetadata = onEditMetadata,
|
||||||
peopleListDescription = peopleList.description,
|
|
||||||
onListRename = onRename,
|
|
||||||
onListDescriptionChange = onDescriptionChange,
|
|
||||||
onListCloneCreate = onClone,
|
onListCloneCreate = onClone,
|
||||||
onListDelete = onDelete,
|
onListDelete = onDelete,
|
||||||
)
|
)
|
||||||
@@ -271,10 +272,7 @@ fun DisplayParticipantNumberAndStatus(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PeopleListOptionsButton(
|
private fun PeopleListOptionsButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
peopleListName: String,
|
onListEditMetadata: () -> Unit,
|
||||||
peopleListDescription: String?,
|
|
||||||
onListRename: (String) -> Unit,
|
|
||||||
onListDescriptionChange: (String?) -> Unit,
|
|
||||||
onListCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit,
|
onListCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit,
|
||||||
onListDelete: () -> Unit,
|
onListDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -286,35 +284,23 @@ private fun PeopleListOptionsButton(
|
|||||||
VerticalDotsIcon()
|
VerticalDotsIcon()
|
||||||
|
|
||||||
ListOptionsMenu(
|
ListOptionsMenu(
|
||||||
setName = peopleListName,
|
|
||||||
setDescription = peopleListDescription,
|
|
||||||
isExpanded = isMenuOpen.value,
|
isExpanded = isMenuOpen.value,
|
||||||
onDismiss = { isMenuOpen.value = false },
|
onListEditMetadata = onListEditMetadata,
|
||||||
onListRename = onListRename,
|
|
||||||
onListDescriptionChange = onListDescriptionChange,
|
|
||||||
onListClone = onListCloneCreate,
|
onListClone = onListCloneCreate,
|
||||||
onDelete = onListDelete,
|
onDelete = onListDelete,
|
||||||
|
onDismiss = { isMenuOpen.value = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ListOptionsMenu(
|
private fun ListOptionsMenu(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
setName: String,
|
onListEditMetadata: () -> Unit,
|
||||||
setDescription: String?,
|
|
||||||
onListRename: (String) -> Unit,
|
|
||||||
onListDescriptionChange: (String?) -> Unit,
|
|
||||||
onListClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit,
|
onListClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val isRenameDialogOpen = remember { mutableStateOf(false) }
|
|
||||||
val renameString = remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
val isDescriptionModDialogOpen = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val isCopyDialogOpen = remember { mutableStateOf(false) }
|
val isCopyDialogOpen = remember { mutableStateOf(false) }
|
||||||
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
val optionalCloneName = remember { mutableStateOf<String?>(null) }
|
||||||
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
val optionalCloneDescription = remember { mutableStateOf<String?>(null) }
|
||||||
@@ -325,19 +311,10 @@ private fun ListOptionsMenu(
|
|||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(text = stringRes(R.string.follow_set_rename_btn_label))
|
Text(text = stringRes(R.string.follow_set_edit_list_metadata))
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
isRenameDialogOpen.value = true
|
onListEditMetadata()
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(text = stringRes(R.string.follow_set_desc_modify_label))
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isDescriptionModDialogOpen.value = true
|
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -360,28 +337,6 @@ private fun ListOptionsMenu(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRenameDialogOpen.value) {
|
|
||||||
ListRenameDialog(
|
|
||||||
currentName = setName,
|
|
||||||
newName = renameString.value,
|
|
||||||
onStringRenameChange = {
|
|
||||||
renameString.value = it
|
|
||||||
},
|
|
||||||
onDismissDialog = { isRenameDialogOpen.value = false },
|
|
||||||
onListRename = {
|
|
||||||
onListRename(renameString.value)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDescriptionModDialogOpen.value) {
|
|
||||||
ListModifyDescriptionDialog(
|
|
||||||
currentDescription = setDescription,
|
|
||||||
onDismissDialog = { isDescriptionModDialogOpen.value = false },
|
|
||||||
onModifyDescription = onListDescriptionChange,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCopyDialogOpen.value) {
|
if (isCopyDialogOpen.value) {
|
||||||
ListCloneDialog(
|
ListCloneDialog(
|
||||||
optionalNewName = optionalCloneName.value,
|
optionalNewName = optionalCloneName.value,
|
||||||
@@ -465,9 +420,9 @@ private fun ListModifyDescriptionDialog(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
currentDescription: String?,
|
currentDescription: String?,
|
||||||
onDismissDialog: () -> Unit,
|
onDismissDialog: () -> Unit,
|
||||||
onModifyDescription: (String?) -> Unit,
|
onModifyDescription: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val updatedDescription = remember { mutableStateOf<String?>(null) }
|
val updatedDescription = remember { mutableStateOf<String>("") }
|
||||||
|
|
||||||
val modifyIndicatorLabel =
|
val modifyIndicatorLabel =
|
||||||
if (currentDescription == null) {
|
if (currentDescription == null) {
|
||||||
@@ -504,7 +459,7 @@ private fun ListModifyDescriptionDialog(
|
|||||||
fontStyle = FontStyle.Italic,
|
fontStyle = FontStyle.Italic,
|
||||||
)
|
)
|
||||||
TextField(
|
TextField(
|
||||||
value = updatedDescription.value ?: "",
|
value = updatedDescription.value,
|
||||||
onValueChange = { updatedDescription.value = it },
|
onValueChange = { updatedDescription.value = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.list
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class PeopleListViewModel : ViewModel() {
|
||||||
|
lateinit var accountViewModel: AccountViewModel
|
||||||
|
|
||||||
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
|
this.accountViewModel = accountViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listFlow() = accountViewModel.account.peopleLists.uiListFlow
|
||||||
|
|
||||||
|
fun cloneItem(
|
||||||
|
followSet: PeopleList,
|
||||||
|
customName: String?,
|
||||||
|
customDescription: String?,
|
||||||
|
) {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
accountViewModel.account.peopleLists.cloneFollowSet(
|
||||||
|
currentPeopleList = followSet,
|
||||||
|
customCloneName = customName,
|
||||||
|
customCloneDescription = customDescription,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteItem(followSet: PeopleList) {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
accountViewModel.account.peopleLists.deleteFollowSet(
|
||||||
|
identifierTag = followSet.identifierTag,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+271
@@ -0,0 +1,271 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.list.metadata
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPackMetadataScreen(
|
||||||
|
selectedDTag: String?,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val postViewModel: FollowPackMetadataViewModel = viewModel()
|
||||||
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
|
if (selectedDTag != null) {
|
||||||
|
LaunchedEffect(postViewModel) {
|
||||||
|
postViewModel.load(selectedDTag)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(postViewModel) {
|
||||||
|
postViewModel.new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FollowPackMetadataScaffold(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||||
|
@Composable
|
||||||
|
private fun DialogContentPreview() {
|
||||||
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
val postViewModel: FollowPackMetadataViewModel = viewModel()
|
||||||
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
|
ThemeComparisonRow {
|
||||||
|
FollowPackMetadataScaffold(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = EmptyNav(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun FollowPackMetadataScaffold(
|
||||||
|
postViewModel: FollowPackMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
FollowPackMetadataTopBar(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { pad ->
|
||||||
|
LazyColumn(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(
|
||||||
|
start = 10.dp,
|
||||||
|
end = 10.dp,
|
||||||
|
top = pad.calculateTopPadding(),
|
||||||
|
bottom = pad.calculateBottomPadding(),
|
||||||
|
).consumeWindowInsets(pad)
|
||||||
|
.imePadding(),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SettingsCategory(
|
||||||
|
R.string.follow_pack_title,
|
||||||
|
R.string.follow_pack_explainer,
|
||||||
|
SettingsCategoryFirstModifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
ListName(postViewModel)
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
|
Picture(postViewModel, accountViewModel)
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
|
Description(postViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPackMetadataTopBar(
|
||||||
|
postViewModel: FollowPackMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
if (postViewModel.isNewPack) {
|
||||||
|
CreatingTopBar(
|
||||||
|
titleRes = R.string.follow_pack_creation_dialog_title,
|
||||||
|
isActive = postViewModel::canPost,
|
||||||
|
onCancel = {
|
||||||
|
postViewModel.clear()
|
||||||
|
nav.popBack()
|
||||||
|
},
|
||||||
|
onPost = {
|
||||||
|
try {
|
||||||
|
postViewModel.createOrUpdate()
|
||||||
|
nav.popBack()
|
||||||
|
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
R.string.read_only_user,
|
||||||
|
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SavingTopBar(
|
||||||
|
titleRes = R.string.follow_pack_edit_list_metadata,
|
||||||
|
isActive = postViewModel::canPost,
|
||||||
|
onCancel = {
|
||||||
|
postViewModel.clear()
|
||||||
|
nav.popBack()
|
||||||
|
},
|
||||||
|
onPost = {
|
||||||
|
try {
|
||||||
|
postViewModel.createOrUpdate()
|
||||||
|
nav.popBack()
|
||||||
|
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
R.string.read_only_user,
|
||||||
|
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Description(postViewModel: FollowPackMetadataViewModel) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.follow_pack_creation_desc_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.description.value,
|
||||||
|
onValueChange = { postViewModel.description.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.about_us),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
minLines = 3,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Picture(
|
||||||
|
postViewModel: FollowPackMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.picture_url)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.picture.value,
|
||||||
|
onValueChange = { postViewModel.picture.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "http://mygroup.com/logo.jpg",
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
val context = LocalContext.current
|
||||||
|
SelectSingleFromGallery(
|
||||||
|
isUploading = postViewModel.isUploadingImageForPicture,
|
||||||
|
tint = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
modifier = Modifier.padding(start = 2.dp),
|
||||||
|
) {
|
||||||
|
postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListName(postViewModel: FollowPackMetadataViewModel) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.follow_pack_creation_name_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.name.value,
|
||||||
|
onValueChange = { postViewModel.name.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.follow_pack_copy_name_label),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
)
|
||||||
|
}
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.list.metadata
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class FollowPackMetadataViewModel : ViewModel() {
|
||||||
|
private lateinit var accountViewModel: AccountViewModel
|
||||||
|
private lateinit var account: Account
|
||||||
|
|
||||||
|
var peopleList by mutableStateOf<PeopleList?>(null)
|
||||||
|
val isNewPack by derivedStateOf { peopleList == null }
|
||||||
|
|
||||||
|
val name = mutableStateOf(TextFieldValue())
|
||||||
|
val picture = mutableStateOf(TextFieldValue())
|
||||||
|
val description = mutableStateOf(TextFieldValue())
|
||||||
|
|
||||||
|
var isUploadingImageForPicture by mutableStateOf(false)
|
||||||
|
|
||||||
|
val canPost by derivedStateOf {
|
||||||
|
name.value.text.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
|
this.accountViewModel = accountViewModel
|
||||||
|
this.account = accountViewModel.account
|
||||||
|
}
|
||||||
|
|
||||||
|
fun new() {
|
||||||
|
peopleList = null
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(dTag: String) {
|
||||||
|
peopleList = account.followLists.selectList(dTag)
|
||||||
|
name.value = TextFieldValue(peopleList?.title ?: "")
|
||||||
|
picture.value = TextFieldValue(peopleList?.image ?: "")
|
||||||
|
description.value = TextFieldValue(peopleList?.description ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createOrUpdate() {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
val peopleList = peopleList
|
||||||
|
if (peopleList == null) {
|
||||||
|
val newListIdentifier =
|
||||||
|
accountViewModel.account.followLists.addFollowList(
|
||||||
|
name = name.value.text,
|
||||||
|
desc = description.value.text,
|
||||||
|
image = picture.value.text,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
accountViewModel.account.followLists.updateMetadata(
|
||||||
|
name = name.value.text,
|
||||||
|
desc = description.value.text,
|
||||||
|
image = picture.value.text,
|
||||||
|
peopleList = peopleList,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
name.value = TextFieldValue()
|
||||||
|
picture.value = TextFieldValue()
|
||||||
|
description.value = TextFieldValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadForPicture(
|
||||||
|
uri: SelectedMedia,
|
||||||
|
context: Context,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
upload(
|
||||||
|
uri,
|
||||||
|
context,
|
||||||
|
onUploading = { isUploadingImageForPicture = it },
|
||||||
|
onUploaded = { picture.value = TextFieldValue(it) },
|
||||||
|
onError = onError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun upload(
|
||||||
|
galleryUri: SelectedMedia,
|
||||||
|
context: Context,
|
||||||
|
onUploading: (Boolean) -> Unit,
|
||||||
|
onUploaded: (String) -> Unit,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
onUploading(true)
|
||||||
|
|
||||||
|
val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result =
|
||||||
|
if (account.settings.defaultFileServer.type == ServerType.NIP96) {
|
||||||
|
Nip96Uploader().upload(
|
||||||
|
uri = compResult.uri,
|
||||||
|
contentType = compResult.contentType,
|
||||||
|
size = compResult.size,
|
||||||
|
alt = null,
|
||||||
|
sensitiveContent = null,
|
||||||
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
|
onProgress = {},
|
||||||
|
httpAuth = account::createHTTPAuthorization,
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
BlossomUploader().upload(
|
||||||
|
uri = compResult.uri,
|
||||||
|
contentType = compResult.contentType,
|
||||||
|
size = compResult.size,
|
||||||
|
alt = null,
|
||||||
|
sensitiveContent = null,
|
||||||
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.url != null) {
|
||||||
|
onUploading(false)
|
||||||
|
onUploaded(result.url)
|
||||||
|
} else {
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
|
||||||
|
}
|
||||||
|
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+271
@@ -0,0 +1,271 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.list.metadata
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PeopleListMetadataScreen(
|
||||||
|
selectedDTag: String?,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val postViewModel: PeopleListMetadataViewModel = viewModel()
|
||||||
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
|
if (selectedDTag != null) {
|
||||||
|
LaunchedEffect(postViewModel) {
|
||||||
|
postViewModel.load(selectedDTag)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(postViewModel) {
|
||||||
|
postViewModel.new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PeopleListMetadataScaffold(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||||
|
@Composable
|
||||||
|
private fun DialogContentPreview() {
|
||||||
|
val accountViewModel = mockAccountViewModel()
|
||||||
|
val postViewModel: PeopleListMetadataViewModel = viewModel()
|
||||||
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
|
ThemeComparisonRow {
|
||||||
|
PeopleListMetadataScaffold(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = EmptyNav(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun PeopleListMetadataScaffold(
|
||||||
|
postViewModel: PeopleListMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
PeopleListMetadataTopBar(
|
||||||
|
postViewModel = postViewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { pad ->
|
||||||
|
LazyColumn(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(
|
||||||
|
start = 10.dp,
|
||||||
|
end = 10.dp,
|
||||||
|
top = pad.calculateTopPadding(),
|
||||||
|
bottom = pad.calculateBottomPadding(),
|
||||||
|
).consumeWindowInsets(pad)
|
||||||
|
.imePadding(),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SettingsCategory(
|
||||||
|
R.string.people_list_title,
|
||||||
|
R.string.people_list_explainer,
|
||||||
|
SettingsCategoryFirstModifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
ListName(postViewModel)
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
|
Picture(postViewModel, accountViewModel)
|
||||||
|
|
||||||
|
Spacer(modifier = DoubleVertSpacer)
|
||||||
|
|
||||||
|
Description(postViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PeopleListMetadataTopBar(
|
||||||
|
postViewModel: PeopleListMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
if (postViewModel.isNewList) {
|
||||||
|
CreatingTopBar(
|
||||||
|
titleRes = R.string.follow_set_creation_dialog_title,
|
||||||
|
isActive = postViewModel::canPost,
|
||||||
|
onCancel = {
|
||||||
|
postViewModel.clear()
|
||||||
|
nav.popBack()
|
||||||
|
},
|
||||||
|
onPost = {
|
||||||
|
try {
|
||||||
|
postViewModel.createOrUpdate()
|
||||||
|
nav.popBack()
|
||||||
|
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
R.string.read_only_user,
|
||||||
|
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SavingTopBar(
|
||||||
|
titleRes = R.string.follow_set_edit_list_metadata,
|
||||||
|
isActive = postViewModel::canPost,
|
||||||
|
onCancel = {
|
||||||
|
postViewModel.clear()
|
||||||
|
nav.popBack()
|
||||||
|
},
|
||||||
|
onPost = {
|
||||||
|
try {
|
||||||
|
postViewModel.createOrUpdate()
|
||||||
|
nav.popBack()
|
||||||
|
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
R.string.read_only_user,
|
||||||
|
R.string.login_with_a_private_key_to_be_able_to_sign_events,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Description(postViewModel: PeopleListMetadataViewModel) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.follow_set_creation_desc_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.description.value,
|
||||||
|
onValueChange = { postViewModel.description.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.about_us),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
minLines = 3,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Picture(
|
||||||
|
postViewModel: PeopleListMetadataViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.picture_url)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.picture.value,
|
||||||
|
onValueChange = { postViewModel.picture.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "http://mygroup.com/logo.jpg",
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
val context = LocalContext.current
|
||||||
|
SelectSingleFromGallery(
|
||||||
|
isUploading = postViewModel.isUploadingImageForPicture,
|
||||||
|
tint = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
modifier = Modifier.padding(start = 2.dp),
|
||||||
|
) {
|
||||||
|
postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListName(postViewModel: PeopleListMetadataViewModel) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringRes(R.string.follow_set_creation_name_label)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = postViewModel.name.value,
|
||||||
|
onValueChange = { postViewModel.name.value = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.follow_set_copy_name_label),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
),
|
||||||
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
)
|
||||||
|
}
|
||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.list.metadata
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleList
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||||
|
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class PeopleListMetadataViewModel : ViewModel() {
|
||||||
|
private lateinit var accountViewModel: AccountViewModel
|
||||||
|
private lateinit var account: Account
|
||||||
|
|
||||||
|
var peopleList by mutableStateOf<PeopleList?>(null)
|
||||||
|
val isNewList by derivedStateOf { peopleList == null }
|
||||||
|
|
||||||
|
val name = mutableStateOf(TextFieldValue())
|
||||||
|
val picture = mutableStateOf(TextFieldValue())
|
||||||
|
val description = mutableStateOf(TextFieldValue())
|
||||||
|
|
||||||
|
var isUploadingImageForPicture by mutableStateOf(false)
|
||||||
|
|
||||||
|
val canPost by derivedStateOf {
|
||||||
|
name.value.text.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(accountViewModel: AccountViewModel) {
|
||||||
|
this.accountViewModel = accountViewModel
|
||||||
|
this.account = accountViewModel.account
|
||||||
|
}
|
||||||
|
|
||||||
|
fun new() {
|
||||||
|
peopleList = null
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(dTag: String) {
|
||||||
|
peopleList = account.peopleLists.selectList(dTag)
|
||||||
|
name.value = TextFieldValue(peopleList?.title ?: "")
|
||||||
|
picture.value = TextFieldValue(peopleList?.image ?: "")
|
||||||
|
description.value = TextFieldValue(peopleList?.description ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isNewChannel() = peopleList == null
|
||||||
|
|
||||||
|
fun createOrUpdate() {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
val peopleList = peopleList
|
||||||
|
if (peopleList == null) {
|
||||||
|
accountViewModel.account.peopleLists.addFollowList(
|
||||||
|
listName = name.value.text,
|
||||||
|
listDescription = description.value.text,
|
||||||
|
listImage = picture.value.text,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
accountViewModel.account.peopleLists.updateMetadata(
|
||||||
|
listName = name.value.text,
|
||||||
|
listDescription = description.value.text,
|
||||||
|
listImage = picture.value.text,
|
||||||
|
peopleList = peopleList,
|
||||||
|
account = accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
name.value = TextFieldValue()
|
||||||
|
picture.value = TextFieldValue()
|
||||||
|
description.value = TextFieldValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadForPicture(
|
||||||
|
uri: SelectedMedia,
|
||||||
|
context: Context,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
upload(
|
||||||
|
uri,
|
||||||
|
context,
|
||||||
|
onUploading = { isUploadingImageForPicture = it },
|
||||||
|
onUploaded = { picture.value = TextFieldValue(it) },
|
||||||
|
onError = onError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun upload(
|
||||||
|
galleryUri: SelectedMedia,
|
||||||
|
context: Context,
|
||||||
|
onUploading: (Boolean) -> Unit,
|
||||||
|
onUploaded: (String) -> Unit,
|
||||||
|
onError: (String, String) -> Unit,
|
||||||
|
) {
|
||||||
|
onUploading(true)
|
||||||
|
|
||||||
|
val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result =
|
||||||
|
if (account.settings.defaultFileServer.type == ServerType.NIP96) {
|
||||||
|
Nip96Uploader().upload(
|
||||||
|
uri = compResult.uri,
|
||||||
|
contentType = compResult.contentType,
|
||||||
|
size = compResult.size,
|
||||||
|
alt = null,
|
||||||
|
sensitiveContent = null,
|
||||||
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
|
onProgress = {},
|
||||||
|
httpAuth = account::createHTTPAuthorization,
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
BlossomUploader().upload(
|
||||||
|
uri = compResult.uri,
|
||||||
|
contentType = compResult.contentType,
|
||||||
|
size = compResult.size,
|
||||||
|
alt = null,
|
||||||
|
sensitiveContent = null,
|
||||||
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.url != null) {
|
||||||
|
onUploading(false)
|
||||||
|
onUploaded(result.url)
|
||||||
|
} else {
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
|
||||||
|
}
|
||||||
|
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
onUploading(false)
|
||||||
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-53
@@ -26,15 +26,8 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.recalculateWindowInsets
|
import androidx.compose.foundation.layout.recalculateWindowInsets
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -49,13 +42,12 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewPeopleListCreationDialog
|
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EditPeopleListScreen(
|
fun FollowListAndPackAndUserScreen(
|
||||||
userToAddOrRemove: HexKey,
|
userToAddOrRemove: HexKey,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
@@ -72,22 +64,19 @@ fun EditPeopleListScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
userBase?.let {
|
userBase?.let {
|
||||||
EditPeopleListScreen(it, accountViewModel, nav)
|
FollowListAndPackAndUserScreen(it, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EditPeopleListScreen(
|
fun FollowListAndPackAndUserScreen(
|
||||||
userToAddOrRemove: User,
|
userToAddOrRemove: User,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize().recalculateWindowInsets(),
|
modifier = Modifier.fillMaxSize().recalculateWindowInsets(),
|
||||||
floatingActionButton = {
|
|
||||||
PeopleListAndUserFab(accountViewModel)
|
|
||||||
},
|
|
||||||
topBar = {
|
topBar = {
|
||||||
val userName by observeUserName(userToAddOrRemove, accountViewModel)
|
val userName by observeUserName(userToAddOrRemove, accountViewModel)
|
||||||
TopBarWithBackButton(
|
TopBarWithBackButton(
|
||||||
@@ -105,45 +94,7 @@ fun EditPeopleListScreen(
|
|||||||
).consumeWindowInsets(contentPadding)
|
).consumeWindowInsets(contentPadding)
|
||||||
.imePadding(),
|
.imePadding(),
|
||||||
) {
|
) {
|
||||||
PeopleListAndUserView(userToAddOrRemove, accountViewModel, nav)
|
FollowListAndPackAndUserView(userToAddOrRemove, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PeopleListAndUserFab(accountViewModel: AccountViewModel) {
|
|
||||||
var isOpen by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
text = {
|
|
||||||
Text(text = stringRes(R.string.follow_set_create_btn_label))
|
|
||||||
},
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { isOpen = !isOpen },
|
|
||||||
shape = CircleShape,
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
NewPeopleListCreationDialog(
|
|
||||||
onDismiss = {
|
|
||||||
isOpen = false
|
|
||||||
},
|
|
||||||
onCreateList = { name, description ->
|
|
||||||
accountViewModel.runIOCatching {
|
|
||||||
accountViewModel.account.peopleLists.addFollowList(
|
|
||||||
listName = name,
|
|
||||||
listDescription = description,
|
|
||||||
account = accountViewModel.account,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isOpen = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+198
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.memberEdit
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowListAndPackAndUserView(
|
||||||
|
userToAddOrRemove: User,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val followSetsState by accountViewModel.account.peopleLists.uiListFlow
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
val followPackFeedState by accountViewModel.account.followLists.uiListFlow
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
if (followSetsState.isEmpty() && followPackFeedState.isEmpty()) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.5f),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(text = stringRes(R.string.follow_set_empty_dialog_msg))
|
||||||
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val userName by observeUserName(userToAddOrRemove, accountViewModel)
|
||||||
|
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
stickyHeader {
|
||||||
|
Row(
|
||||||
|
modifier = MaxWidthWithHorzPadding,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = SpacedBy5dp,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.follow_sets),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.follow_sets_explainer),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
NewListButton(
|
||||||
|
onClick = { nav.nav(Route.PeopleListMetadataEdit()) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemsIndexed(followSetsState, key = { _, item -> item.identifierTag }) { _, list ->
|
||||||
|
PeopleListAndUserItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
listHeader = list.title,
|
||||||
|
userName = userName,
|
||||||
|
userIsPrivateMember = list.privateMembers.contains(userToAddOrRemove),
|
||||||
|
userIsPublicMember = list.publicMembers.contains(userToAddOrRemove),
|
||||||
|
onRemoveUser = {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
accountViewModel.account.peopleLists.removeUserFromSet(
|
||||||
|
userToAddOrRemove,
|
||||||
|
isPrivate = list.privateMembers.contains(userToAddOrRemove),
|
||||||
|
list.identifierTag,
|
||||||
|
accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
privateMemberSize = list.privateMembers.size,
|
||||||
|
publicMemberSize = list.publicMembers.size,
|
||||||
|
onClick = {
|
||||||
|
nav.nav(Route.MyPeopleListView(list.identifierTag))
|
||||||
|
},
|
||||||
|
onAddUserToList = { userShouldBePrivate ->
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
accountViewModel.account.peopleLists.addUserToSet(
|
||||||
|
userToAddOrRemove,
|
||||||
|
list.identifierTag,
|
||||||
|
userShouldBePrivate,
|
||||||
|
accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
}
|
||||||
|
stickyHeader {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = SpacedBy5dp,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.discover_follows),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.discover_follows_explainer),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.grayText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
NewListButton(
|
||||||
|
onClick = { nav.nav(Route.FollowPackMetadataEdit()) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemsIndexed(followPackFeedState, key = { _, item -> item.identifierTag }) { _, list ->
|
||||||
|
FollowPackAndUserItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
listHeader = list.title,
|
||||||
|
userName = userName,
|
||||||
|
isMember = list.publicMembers.contains(userToAddOrRemove),
|
||||||
|
onClick = {
|
||||||
|
nav.nav(Route.MyFollowPackView(list.identifierTag))
|
||||||
|
},
|
||||||
|
onRemoveUser = {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
accountViewModel.account.followLists.removeUserFromSet(
|
||||||
|
userToAddOrRemove,
|
||||||
|
list.identifierTag,
|
||||||
|
accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
memberSize = list.publicMembers.size,
|
||||||
|
onAddUserToList = {
|
||||||
|
accountViewModel.launchSigner {
|
||||||
|
accountViewModel.account.followLists.addUserToSet(
|
||||||
|
userToAddOrRemove,
|
||||||
|
list.identifierTag,
|
||||||
|
accountViewModel.account,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+216
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* 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.lists.memberEdit
|
||||||
|
|
||||||
|
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.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PersonAdd
|
||||||
|
import androidx.compose.material.icons.filled.PersonRemove
|
||||||
|
import androidx.compose.material.icons.outlined.Groups
|
||||||
|
import androidx.compose.material.icons.outlined.Public
|
||||||
|
import androidx.compose.material.icons.outlined.RemoveCircleOutline
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.DisplayParticipantNumberAndStatus
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size50ModifierOffset10
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun FollowPackAndUserMemberPreview() {
|
||||||
|
ThemeComparisonColumn {
|
||||||
|
FollowPackAndUserItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
listHeader = "list title",
|
||||||
|
userName = "User",
|
||||||
|
isMember = true,
|
||||||
|
memberSize = 2,
|
||||||
|
onAddUserToList = {},
|
||||||
|
onClick = {},
|
||||||
|
onRemoveUser = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun FollowPackAndUserNotMemberPreview() {
|
||||||
|
ThemeComparisonColumn {
|
||||||
|
FollowPackAndUserItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
listHeader = "list title",
|
||||||
|
userName = "User",
|
||||||
|
isMember = false,
|
||||||
|
memberSize = 2,
|
||||||
|
onAddUserToList = {},
|
||||||
|
onClick = {},
|
||||||
|
onRemoveUser = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowPackAndUserItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
listHeader: String,
|
||||||
|
userName: String,
|
||||||
|
isMember: Boolean,
|
||||||
|
memberSize: Int,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onAddUserToList: () -> Unit,
|
||||||
|
onRemoveUser: () -> Unit,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = listHeader,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
UserStatusInList(userName, isMember)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Groups,
|
||||||
|
contentDescription = stringRes(R.string.follow_set_icon_description),
|
||||||
|
modifier = Size50ModifierOffset10,
|
||||||
|
)
|
||||||
|
DisplayParticipantNumberAndStatus(
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
privateMembersSize = 0,
|
||||||
|
publicMembersSize = memberSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
UserAdditionOptions(isMember, onAddUserToList, onRemoveUser)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UserStatusInList(
|
||||||
|
userName: String,
|
||||||
|
isMember: Boolean,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = HalfHalfVertPadding,
|
||||||
|
horizontalArrangement = SpacedBy5dp,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val text =
|
||||||
|
if (isMember) {
|
||||||
|
stringRes(R.string.follow_set_public_presence_indicator, userName)
|
||||||
|
} else {
|
||||||
|
stringRes(R.string.follow_set_absence_indicator2, userName)
|
||||||
|
}
|
||||||
|
|
||||||
|
val icon =
|
||||||
|
if (isMember) {
|
||||||
|
Icons.Outlined.Public
|
||||||
|
} else {
|
||||||
|
Icons.Outlined.RemoveCircleOutline
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = text,
|
||||||
|
modifier = Size15Modifier,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
overflow = TextOverflow.MiddleEllipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UserAdditionOptions(
|
||||||
|
isUserInList: Boolean,
|
||||||
|
onAddUserToList: () -> Unit,
|
||||||
|
onRemoveUser: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (isUserInList) {
|
||||||
|
onRemoveUser()
|
||||||
|
} else {
|
||||||
|
onAddUserToList()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.background(
|
||||||
|
color =
|
||||||
|
if (isUserInList) {
|
||||||
|
MaterialTheme.colorScheme.errorContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(percent = 80),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
if (isUserInList) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PersonRemove,
|
||||||
|
contentDescription = stringRes(R.string.remove_user_from_the_list),
|
||||||
|
tint = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PersonAdd,
|
||||||
|
contentDescription = stringRes(R.string.add_user_to_the_list),
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -70,6 +71,7 @@ fun PeopleListAndUserMemberPreview() {
|
|||||||
privateMemberSize = 3,
|
privateMemberSize = 3,
|
||||||
publicMemberSize = 2,
|
publicMemberSize = 2,
|
||||||
onAddUserToList = {},
|
onAddUserToList = {},
|
||||||
|
onClick = {},
|
||||||
onRemoveUser = {},
|
onRemoveUser = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -88,6 +90,7 @@ fun PeopleListAndUserNotMemberPreview() {
|
|||||||
privateMemberSize = 3,
|
privateMemberSize = 3,
|
||||||
publicMemberSize = 2,
|
publicMemberSize = 2,
|
||||||
onAddUserToList = {},
|
onAddUserToList = {},
|
||||||
|
onClick = {},
|
||||||
onRemoveUser = {},
|
onRemoveUser = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -102,11 +105,12 @@ fun PeopleListAndUserItem(
|
|||||||
userIsPublicMember: Boolean,
|
userIsPublicMember: Boolean,
|
||||||
publicMemberSize: Int,
|
publicMemberSize: Int,
|
||||||
privateMemberSize: Int,
|
privateMemberSize: Int,
|
||||||
|
onClick: () -> Unit,
|
||||||
onAddUserToList: (shouldBePrivateMember: Boolean) -> Unit,
|
onAddUserToList: (shouldBePrivateMember: Boolean) -> Unit,
|
||||||
onRemoveUser: () -> Unit,
|
onRemoveUser: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = modifier,
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
text = listHeader,
|
text = listHeader,
|
||||||
@@ -139,7 +143,7 @@ fun PeopleListAndUserItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserStatusInList(
|
private fun UserStatusInList(
|
||||||
userName: String,
|
userName: String,
|
||||||
userIsPrivateMember: Boolean,
|
userIsPrivateMember: Boolean,
|
||||||
userIsPublicMember: Boolean,
|
userIsPublicMember: Boolean,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user