feat: add pinned notes support (NIP-51 kind 10001)
Implement pinned notes using NIP-51 kind 10001 event. This allows users to pin notes to their profile and view pinned notes on other profiles. - Rewrite PinListEvent from non-standard kind 33888 to NIP-51 kind 10001 using e tags and BaseReplaceableEvent - Add PinListState for reactive pin state management - Add pin/unpin actions to Account, AccountViewModel, and 3-dot menu - Add Pinned Notes tab to user profile page - Add Pinned Notes tab to My Bookmarks screen - Subscribe to kind 10001 in account metadata and profile relay filters https://claude.ai/code/session_012GQzb2qcGfAcizC6jqZArD
This commit is contained in:
@@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
|
|||||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
|
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
|
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||||
|
import com.vitorpamplona.amethyst.model.nip51Lists.PinListState
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleListState
|
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleListState
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
|
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
|
||||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||||
@@ -318,6 +319,7 @@ class Account(
|
|||||||
|
|
||||||
val labeledBookmarkLists = LabeledBookmarkListsState(signer, cache, scope)
|
val labeledBookmarkLists = LabeledBookmarkListsState(signer, cache, scope)
|
||||||
val bookmarkState = BookmarkListState(signer, cache, scope)
|
val bookmarkState = BookmarkListState(signer, cache, scope)
|
||||||
|
val pinState = PinListState(signer, cache, scope)
|
||||||
val emoji = EmojiPackState(signer, cache, scope)
|
val emoji = EmojiPackState(signer, cache, scope)
|
||||||
|
|
||||||
val appSpecific = AppSpecificState(signer, cache, scope, settings)
|
val appSpecific = AppSpecificState(signer, cache, scope, settings)
|
||||||
@@ -1809,6 +1811,43 @@ class Account(
|
|||||||
cache.justConsumeMyOwnEvent(event)
|
cache.justConsumeMyOwnEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun addPin(note: Note) {
|
||||||
|
if (!isWriteable() || note.isDraft()) return
|
||||||
|
|
||||||
|
sendMyPublicAndPrivateOutbox(pinState.addPin(note))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removePin(note: Note) {
|
||||||
|
if (!isWriteable() || note.isDraft()) return
|
||||||
|
|
||||||
|
val event = pinState.removePin(note)
|
||||||
|
if (event != null) {
|
||||||
|
sendMyPublicAndPrivateOutbox(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createAddPinEvent(note: Note): Pair<Event, Set<NormalizedRelayUrl>>? {
|
||||||
|
if (!isWriteable() || note.isDraft()) return null
|
||||||
|
|
||||||
|
val event = pinState.addPin(note)
|
||||||
|
val relays = outboxRelays.flow.value
|
||||||
|
|
||||||
|
return event to relays
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createRemovePinEvent(note: Note): Pair<Event, Set<NormalizedRelayUrl>>? {
|
||||||
|
if (!isWriteable() || note.isDraft()) return null
|
||||||
|
|
||||||
|
val event = pinState.removePin(note) ?: return null
|
||||||
|
val relays = outboxRelays.flow.value
|
||||||
|
|
||||||
|
return event to relays
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumePinEvent(event: Event) {
|
||||||
|
cache.justConsumeMyOwnEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun createAuthEvent(
|
suspend fun createAuthEvent(
|
||||||
relay: NormalizedRelayUrl,
|
relay: NormalizedRelayUrl,
|
||||||
challenge: String,
|
challenge: String,
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip51Lists
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.NoteState
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class PinListState(
|
||||||
|
val signer: NostrSigner,
|
||||||
|
val cache: LocalCache,
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
val pinList = cache.getOrCreateAddressableNote(PinListEvent.createPinAddress(signer.pubKey))
|
||||||
|
|
||||||
|
fun getPinListFlow(): StateFlow<NoteState> = pinList.flow().metadata.stateFlow
|
||||||
|
|
||||||
|
fun getPinList(): PinListEvent? = pinList.event as? PinListEvent
|
||||||
|
|
||||||
|
fun pinnedEvents(note: Note): List<EventBookmark> {
|
||||||
|
val noteEvent = note.event as? PinListEvent
|
||||||
|
return noteEvent?.pinnedEvents() ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
val pinnedNotes: StateFlow<List<EventBookmark>> =
|
||||||
|
getPinListFlow()
|
||||||
|
.map { noteState ->
|
||||||
|
pinnedEvents(noteState.note)
|
||||||
|
}.onStart {
|
||||||
|
emit(pinnedEvents(pinList))
|
||||||
|
}.debounce(100)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.Eagerly,
|
||||||
|
emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val pinnedEventIdSet: StateFlow<Set<String>> =
|
||||||
|
pinnedNotes
|
||||||
|
.map { pins ->
|
||||||
|
pins.map { it.eventId }.toSet()
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.Eagerly,
|
||||||
|
emptySet(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
val pinnedNotesList: StateFlow<List<Note>> =
|
||||||
|
pinnedNotes
|
||||||
|
.map { pins ->
|
||||||
|
pins.mapNotNull { cache.checkGetOrCreateNote(it.eventId) }.reversed()
|
||||||
|
}.onStart {
|
||||||
|
emit(
|
||||||
|
pinnedNotes.value.mapNotNull { cache.checkGetOrCreateNote(it.eventId) }.reversed(),
|
||||||
|
)
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.Eagerly,
|
||||||
|
emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun isPinned(note: Note): Boolean = pinnedEventIdSet.value.contains(note.idHex)
|
||||||
|
|
||||||
|
suspend fun addPin(note: Note): PinListEvent {
|
||||||
|
val currentList = getPinList()
|
||||||
|
val pin = EventBookmark(note.idHex, note.relayHintUrl())
|
||||||
|
|
||||||
|
return if (currentList == null) {
|
||||||
|
PinListEvent.create(
|
||||||
|
pin = pin,
|
||||||
|
signer = signer,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PinListEvent.add(
|
||||||
|
earlierVersion = currentList,
|
||||||
|
pin = pin,
|
||||||
|
signer = signer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removePin(note: Note): PinListEvent? {
|
||||||
|
val currentList = getPinList() ?: return null
|
||||||
|
val pin = EventBookmark(note.idHex, note.relayHintUrl())
|
||||||
|
|
||||||
|
return PinListEvent.remove(
|
||||||
|
earlierVersion = currentList,
|
||||||
|
pin = pin,
|
||||||
|
signer = signer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ val ReportsAndBookmarksFromKeyKinds =
|
|||||||
listOf(
|
listOf(
|
||||||
ReportEvent.KIND,
|
ReportEvent.KIND,
|
||||||
BookmarkListEvent.KIND,
|
BookmarkListEvent.KIND,
|
||||||
|
PinListEvent.KIND,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun filterBookmarksAndReportsFromKey(
|
fun filterBookmarksAndReportsFromKey(
|
||||||
|
|||||||
+25
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -276,6 +277,30 @@ fun observeUserBookmarkCount(
|
|||||||
return flow.collectAsStateWithLifecycle(0)
|
return flow.collectAsStateWithLifecycle(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||||
|
@Composable
|
||||||
|
fun observeUserPinnedNotesCount(
|
||||||
|
user: User,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
): State<Int> {
|
||||||
|
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||||
|
|
||||||
|
val flow =
|
||||||
|
remember(user) {
|
||||||
|
accountViewModel
|
||||||
|
.pinnedNotes(user)
|
||||||
|
.flow()
|
||||||
|
.metadata.stateFlow
|
||||||
|
.sample(200)
|
||||||
|
.mapLatest { noteState ->
|
||||||
|
(noteState.note.event as? PinListEvent)?.countPins() ?: 0
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
return flow.collectAsStateWithLifecycle(0)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun observeUserIsFollowing(
|
fun observeUserIsFollowing(
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.compose.material.icons.outlined.LockOpen
|
|||||||
import androidx.compose.material.icons.outlined.PersonAdd
|
import androidx.compose.material.icons.outlined.PersonAdd
|
||||||
import androidx.compose.material.icons.outlined.PersonRemove
|
import androidx.compose.material.icons.outlined.PersonRemove
|
||||||
import androidx.compose.material.icons.outlined.PlaylistAdd
|
import androidx.compose.material.icons.outlined.PlaylistAdd
|
||||||
|
import androidx.compose.material.icons.outlined.PushPin
|
||||||
import androidx.compose.material.icons.outlined.Report
|
import androidx.compose.material.icons.outlined.Report
|
||||||
import androidx.compose.material.icons.outlined.Schedule
|
import androidx.compose.material.icons.outlined.Schedule
|
||||||
import androidx.compose.material.icons.outlined.Share
|
import androidx.compose.material.icons.outlined.Share
|
||||||
@@ -110,6 +111,7 @@ data class DropDownParams(
|
|||||||
val isFollowingAuthor: Boolean,
|
val isFollowingAuthor: Boolean,
|
||||||
val isPrivateBookmarkNote: Boolean,
|
val isPrivateBookmarkNote: Boolean,
|
||||||
val isPublicBookmarkNote: Boolean,
|
val isPublicBookmarkNote: Boolean,
|
||||||
|
val isPinnedNote: Boolean,
|
||||||
val isLoggedUser: Boolean,
|
val isLoggedUser: Boolean,
|
||||||
val isSensitive: Boolean,
|
val isSensitive: Boolean,
|
||||||
val showSensitiveContent: Boolean?,
|
val showSensitiveContent: Boolean?,
|
||||||
@@ -130,6 +132,7 @@ fun NoteDropDownMenu(
|
|||||||
isFollowingAuthor = false,
|
isFollowingAuthor = false,
|
||||||
isPrivateBookmarkNote = false,
|
isPrivateBookmarkNote = false,
|
||||||
isPublicBookmarkNote = false,
|
isPublicBookmarkNote = false,
|
||||||
|
isPinnedNote = false,
|
||||||
isLoggedUser = false,
|
isLoggedUser = false,
|
||||||
isSensitive = false,
|
isSensitive = false,
|
||||||
showSensitiveContent = null,
|
showSensitiveContent = null,
|
||||||
@@ -265,6 +268,19 @@ fun NoteDropDownMenu(
|
|||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (state.isLoggedUser) {
|
||||||
|
if (state.isPinnedNote) {
|
||||||
|
M3ActionRow(icon = Icons.Outlined.PushPin, text = stringRes(R.string.unpin_from_profile)) {
|
||||||
|
accountViewModel.removePin(note)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
M3ActionRow(icon = Icons.Outlined.PushPin, text = stringRes(R.string.pin_to_profile)) {
|
||||||
|
accountViewModel.addPin(note)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
val noteBookmarkType = if (note.event is LongTextNoteEvent) stringRes(R.string.article) else stringRes(R.string.post)
|
val noteBookmarkType = if (note.event is LongTextNoteEvent) stringRes(R.string.article) else stringRes(R.string.post)
|
||||||
M3ActionRow(icon = Icons.Outlined.BookmarkAdd, text = stringRes(R.string.manage_bookmark_label, noteBookmarkType)) {
|
M3ActionRow(icon = Icons.Outlined.BookmarkAdd, text = stringRes(R.string.manage_bookmark_label, noteBookmarkType)) {
|
||||||
if (note.event is LongTextNoteEvent) {
|
if (note.event is LongTextNoteEvent) {
|
||||||
@@ -329,12 +345,14 @@ fun observeBookmarksFollowsAndAccount(
|
|||||||
combine(
|
combine(
|
||||||
accountViewModel.account.kind3FollowList.flow,
|
accountViewModel.account.kind3FollowList.flow,
|
||||||
accountViewModel.account.bookmarkState.bookmarks,
|
accountViewModel.account.bookmarkState.bookmarks,
|
||||||
|
accountViewModel.account.pinState.pinnedEventIdSet,
|
||||||
accountViewModel.showSensitiveContent(),
|
accountViewModel.showSensitiveContent(),
|
||||||
) { follows, bookmarks, showSensitiveContent ->
|
) { follows, bookmarks, pinnedIds, showSensitiveContent ->
|
||||||
DropDownParams(
|
DropDownParams(
|
||||||
isFollowingAuthor = note.author?.pubkeyHex in follows.authors,
|
isFollowingAuthor = note.author?.pubkeyHex in follows.authors,
|
||||||
isPrivateBookmarkNote = note in bookmarks.private,
|
isPrivateBookmarkNote = note in bookmarks.private,
|
||||||
isPublicBookmarkNote = note in bookmarks.public,
|
isPublicBookmarkNote = note in bookmarks.public,
|
||||||
|
isPinnedNote = note.idHex in pinnedIds,
|
||||||
isLoggedUser = accountViewModel.isLoggedUser(note.author),
|
isLoggedUser = accountViewModel.isLoggedUser(note.author),
|
||||||
isSensitive = note.event?.isSensitiveOrNSFW() ?: false,
|
isSensitive = note.event?.isSensitiveOrNSFW() ?: false,
|
||||||
showSensitiveContent = showSensitiveContent,
|
showSensitiveContent = showSensitiveContent,
|
||||||
@@ -345,6 +363,7 @@ fun observeBookmarksFollowsAndAccount(
|
|||||||
isFollowingAuthor = note.author?.pubkeyHex?.let { accountViewModel.account.isFollowing(it) } ?: false,
|
isFollowingAuthor = note.author?.pubkeyHex?.let { accountViewModel.account.isFollowing(it) } ?: false,
|
||||||
isPrivateBookmarkNote = note in accountViewModel.account.bookmarkState.bookmarks.value.private,
|
isPrivateBookmarkNote = note in accountViewModel.account.bookmarkState.bookmarks.value.private,
|
||||||
isPublicBookmarkNote = note in accountViewModel.account.bookmarkState.bookmarks.value.public,
|
isPublicBookmarkNote = note in accountViewModel.account.bookmarkState.bookmarks.value.public,
|
||||||
|
isPinnedNote = accountViewModel.account.pinState.isPinned(note),
|
||||||
isLoggedUser = accountViewModel.isLoggedUser(note.author),
|
isLoggedUser = accountViewModel.isLoggedUser(note.author),
|
||||||
isSensitive = note.event?.isSensitiveOrNSFW() ?: false,
|
isSensitive = note.event?.isSensitiveOrNSFW() ?: false,
|
||||||
showSensitiveContent = accountViewModel.showSensitiveContent().value,
|
showSensitiveContent = accountViewModel.showSensitiveContent().value,
|
||||||
|
|||||||
@@ -45,15 +45,14 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
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.unit.dp
|
||||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
|
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
|
||||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.PinIcon
|
import com.vitorpamplona.amethyst.ui.note.PinIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.getGradient
|
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.theme.Size15Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||||
|
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@@ -66,7 +65,7 @@ fun RenderPinListEvent(
|
|||||||
) {
|
) {
|
||||||
val noteEvent = baseNote.event as? PinListEvent ?: return
|
val noteEvent = baseNote.event as? PinListEvent ?: return
|
||||||
|
|
||||||
val pins by remember { mutableStateOf(noteEvent.pins()) }
|
val pins by remember { mutableStateOf(noteEvent.pinnedEvents()) }
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -78,7 +77,7 @@ fun RenderPinListEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "#${noteEvent.dTag()}",
|
text = "Pinned Notes",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@@ -103,15 +102,10 @@ fun RenderPinListEvent(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
Spacer(modifier = Modifier.width(5.dp))
|
||||||
|
|
||||||
TranslatableRichTextViewer(
|
Text(
|
||||||
content = pin,
|
text = NEvent.create(pin.eventId, pin.author, null, pin.relay),
|
||||||
canPreview = true,
|
maxLines = 1,
|
||||||
quotesLeft = 1,
|
overflow = TextOverflow.Ellipsis,
|
||||||
tags = EmptyTagList,
|
|
||||||
backgroundColor = backgroundColor,
|
|
||||||
id = baseNote.idHex,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
@@ -129,6 +129,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
|||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||||
@@ -846,6 +847,42 @@ class AccountViewModel(
|
|||||||
|
|
||||||
fun bookmarks(user: User): Note = LocalCache.getOrCreateAddressableNote(BookmarkListEvent.createBookmarkAddress(user.pubkeyHex))
|
fun bookmarks(user: User): Note = LocalCache.getOrCreateAddressableNote(BookmarkListEvent.createBookmarkAddress(user.pubkeyHex))
|
||||||
|
|
||||||
|
fun pinnedNotes(user: User): Note = LocalCache.getOrCreateAddressableNote(PinListEvent.createPinAddress(user.pubkeyHex))
|
||||||
|
|
||||||
|
fun addPin(note: Note) {
|
||||||
|
if (settings.isCompleteUIMode()) {
|
||||||
|
launchSigner {
|
||||||
|
account.createAddPinEvent(note)?.let { (event, relays) ->
|
||||||
|
broadcastTracker.trackBroadcast(
|
||||||
|
event = event,
|
||||||
|
relays = relays,
|
||||||
|
client = account.client,
|
||||||
|
)
|
||||||
|
account.consumePinEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
launchSigner { account.addPin(note) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removePin(note: Note) {
|
||||||
|
if (settings.isCompleteUIMode()) {
|
||||||
|
launchSigner {
|
||||||
|
account.createRemovePinEvent(note)?.let { (event, relays) ->
|
||||||
|
broadcastTracker.trackBroadcast(
|
||||||
|
event = event,
|
||||||
|
relays = relays,
|
||||||
|
client = account.client,
|
||||||
|
)
|
||||||
|
account.consumePinEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
launchSigner { account.removePin(note) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addPrivateBookmark(note: Note) {
|
fun addPrivateBookmark(note: Note) {
|
||||||
if (settings.isCompleteUIMode()) {
|
if (settings.isCompleteUIMode()) {
|
||||||
launchSigner {
|
launchSigner {
|
||||||
|
|||||||
+35
-4
@@ -27,8 +27,8 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.TabRow
|
|
||||||
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.LaunchedEffect
|
||||||
@@ -36,6 +36,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.unit.dp
|
||||||
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
|
||||||
@@ -46,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal.BookmarkPrivateFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal.BookmarkPrivateFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal.BookmarkPublicFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal.BookmarkPublicFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal.PinnedNotesFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -67,15 +69,28 @@ fun BookmarkListScreen(
|
|||||||
factory = BookmarkPrivateFeedViewModel.Factory(accountViewModel.account),
|
factory = BookmarkPrivateFeedViewModel.Factory(accountViewModel.account),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val pinnedNotesFeedViewModel: PinnedNotesFeedViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = "NostrPinnedNotesFeedViewModel",
|
||||||
|
factory = PinnedNotesFeedViewModel.Factory(accountViewModel.account),
|
||||||
|
)
|
||||||
|
|
||||||
val bookmarkState by accountViewModel.account.bookmarkState.bookmarks
|
val bookmarkState by accountViewModel.account.bookmarkState.bookmarks
|
||||||
.collectAsStateWithLifecycle(null)
|
.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
val pinState by accountViewModel.account.pinState.pinnedNotesList
|
||||||
|
.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
LaunchedEffect(bookmarkState) {
|
LaunchedEffect(bookmarkState) {
|
||||||
publicFeedViewModel.invalidateData()
|
publicFeedViewModel.invalidateData()
|
||||||
privateFeedViewModel.invalidateData()
|
privateFeedViewModel.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderBookmarkScreen(publicFeedViewModel, privateFeedViewModel, accountViewModel, nav)
|
LaunchedEffect(pinState) {
|
||||||
|
pinnedNotesFeedViewModel.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderBookmarkScreen(publicFeedViewModel, privateFeedViewModel, pinnedNotesFeedViewModel, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -83,10 +98,11 @@ fun BookmarkListScreen(
|
|||||||
private fun RenderBookmarkScreen(
|
private fun RenderBookmarkScreen(
|
||||||
publicFeedViewModel: BookmarkPublicFeedViewModel,
|
publicFeedViewModel: BookmarkPublicFeedViewModel,
|
||||||
privateFeedViewModel: BookmarkPrivateFeedViewModel,
|
privateFeedViewModel: BookmarkPrivateFeedViewModel,
|
||||||
|
pinnedNotesFeedViewModel: PinnedNotesFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val pagerState = rememberPagerState { 2 }
|
val pagerState = rememberPagerState { 3 }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
DisappearingScaffold(
|
DisappearingScaffold(
|
||||||
@@ -94,10 +110,11 @@ private fun RenderBookmarkScreen(
|
|||||||
topBar = {
|
topBar = {
|
||||||
Column {
|
Column {
|
||||||
TopBarWithBackButton(stringRes(id = R.string.bookmarks_title), nav::popBack)
|
TopBarWithBackButton(stringRes(id = R.string.bookmarks_title), nav::popBack)
|
||||||
TabRow(
|
ScrollableTabRow(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
edgePadding = 8.dp,
|
||||||
modifier = TabRowHeight,
|
modifier = TabRowHeight,
|
||||||
) {
|
) {
|
||||||
Tab(
|
Tab(
|
||||||
@@ -110,6 +127,11 @@ private fun RenderBookmarkScreen(
|
|||||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
||||||
text = { Text(text = stringRes(R.string.public_bookmarks)) },
|
text = { Text(text = stringRes(R.string.public_bookmarks)) },
|
||||||
)
|
)
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 2,
|
||||||
|
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } },
|
||||||
|
text = { Text(text = stringRes(R.string.pinned_notes)) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -135,6 +157,15 @@ private fun RenderBookmarkScreen(
|
|||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
RefresheableFeedView(
|
||||||
|
pinnedNotesFeedViewModel,
|
||||||
|
null,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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.bookmarkgroups.default.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||||
|
|
||||||
|
class PinnedNotesFeedFilter(
|
||||||
|
val account: Account,
|
||||||
|
) : FeedFilter<Note>() {
|
||||||
|
override fun feedKey(): String =
|
||||||
|
account.pinState.pinnedNotesList.value
|
||||||
|
.hashCode()
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
override fun feed(): List<Note> = account.pinState.pinnedNotesList.value
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.bookmarkgroups.default.dal
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class PinnedNotesFeedViewModel(
|
||||||
|
val account: Account,
|
||||||
|
) : AndroidFeedViewModel(PinnedNotesFeedFilter(account)) {
|
||||||
|
class Factory(
|
||||||
|
val account: Account,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = PinnedNotesFeedViewModel(account) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -125,6 +125,7 @@ fun BookmarkGroupItemOptionsMenu(
|
|||||||
isFollowingAuthor = false,
|
isFollowingAuthor = false,
|
||||||
isPrivateBookmarkNote = false,
|
isPrivateBookmarkNote = false,
|
||||||
isPublicBookmarkNote = false,
|
isPublicBookmarkNote = false,
|
||||||
|
isPinnedNote = false,
|
||||||
isLoggedUser = false,
|
isLoggedUser = false,
|
||||||
isSensitive = false,
|
isSensitive = false,
|
||||||
showSensitiveContent = null,
|
showSensitiveContent = null,
|
||||||
|
|||||||
+28
-4
@@ -87,6 +87,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.TabMutualCon
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.TabNotesNewThreads
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.TabNotesNewThreads
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.PinnedNotesTabHeader
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.TabPinnedNotes
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.RelaysTabHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.RelaysTabHeader
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.TabRelays
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.TabRelays
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.ReportsTabHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.ReportsTabHeader
|
||||||
@@ -223,6 +226,16 @@ fun PrepareViewModels(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val pinnedNotesFeedViewModel: UserProfilePinnedNotesFeedViewModel =
|
||||||
|
viewModel(
|
||||||
|
key = baseUser.pubkeyHex + "UserProfilePinnedNotesFeedViewModel",
|
||||||
|
factory =
|
||||||
|
UserProfilePinnedNotesFeedViewModel.Factory(
|
||||||
|
baseUser,
|
||||||
|
accountViewModel.account,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
val reportsFeedViewModel: UserProfileReportFeedViewModel =
|
val reportsFeedViewModel: UserProfileReportFeedViewModel =
|
||||||
viewModel(
|
viewModel(
|
||||||
key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel",
|
key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel",
|
||||||
@@ -244,6 +257,7 @@ fun PrepareViewModels(
|
|||||||
externalIdentities,
|
externalIdentities,
|
||||||
zapFeedViewModel,
|
zapFeedViewModel,
|
||||||
bookmarksFeedViewModel,
|
bookmarksFeedViewModel,
|
||||||
|
pinnedNotesFeedViewModel,
|
||||||
galleryFeedViewModel,
|
galleryFeedViewModel,
|
||||||
reportsFeedViewModel,
|
reportsFeedViewModel,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
@@ -263,6 +277,7 @@ fun ProfileScreen(
|
|||||||
externalIdentities: UserExternalIdentitiesViewModel,
|
externalIdentities: UserExternalIdentitiesViewModel,
|
||||||
zapFeedViewModel: UserProfileZapsViewModel,
|
zapFeedViewModel: UserProfileZapsViewModel,
|
||||||
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
||||||
|
pinnedNotesFeedViewModel: UserProfilePinnedNotesFeedViewModel,
|
||||||
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
||||||
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
@@ -273,6 +288,7 @@ fun ProfileScreen(
|
|||||||
WatchLifecycleAndUpdateModel(mutualViewModel)
|
WatchLifecycleAndUpdateModel(mutualViewModel)
|
||||||
WatchLifecycleAndUpdateModel(appRecommendations)
|
WatchLifecycleAndUpdateModel(appRecommendations)
|
||||||
WatchLifecycleAndUpdateModel(bookmarksFeedViewModel)
|
WatchLifecycleAndUpdateModel(bookmarksFeedViewModel)
|
||||||
|
WatchLifecycleAndUpdateModel(pinnedNotesFeedViewModel)
|
||||||
WatchLifecycleAndUpdateModel(galleryFeedViewModel)
|
WatchLifecycleAndUpdateModel(galleryFeedViewModel)
|
||||||
|
|
||||||
UserProfileFilterAssemblerSubscription(baseUser, accountViewModel.dataSources().profile)
|
UserProfileFilterAssemblerSubscription(baseUser, accountViewModel.dataSources().profile)
|
||||||
@@ -291,6 +307,7 @@ fun ProfileScreen(
|
|||||||
followersFeedViewModel,
|
followersFeedViewModel,
|
||||||
zapFeedViewModel,
|
zapFeedViewModel,
|
||||||
bookmarksFeedViewModel,
|
bookmarksFeedViewModel,
|
||||||
|
pinnedNotesFeedViewModel,
|
||||||
galleryFeedViewModel,
|
galleryFeedViewModel,
|
||||||
reportsFeedViewModel,
|
reportsFeedViewModel,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
@@ -385,12 +402,13 @@ private fun RenderScreen(
|
|||||||
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
||||||
zapFeedViewModel: UserProfileZapsViewModel,
|
zapFeedViewModel: UserProfileZapsViewModel,
|
||||||
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
||||||
|
pinnedNotesFeedViewModel: UserProfilePinnedNotesFeedViewModel,
|
||||||
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
||||||
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val pagerState = rememberPagerState { 11 }
|
val pagerState = rememberPagerState { 12 }
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
ProfileHeader(baseUser, appRecommendations, externalIdentities, nav, accountViewModel)
|
ProfileHeader(baseUser, appRecommendations, externalIdentities, nav, accountViewModel)
|
||||||
@@ -412,6 +430,7 @@ private fun RenderScreen(
|
|||||||
followersFeedViewModel,
|
followersFeedViewModel,
|
||||||
zapFeedViewModel,
|
zapFeedViewModel,
|
||||||
bookmarksFeedViewModel,
|
bookmarksFeedViewModel,
|
||||||
|
pinnedNotesFeedViewModel,
|
||||||
galleryFeedViewModel,
|
galleryFeedViewModel,
|
||||||
reportsFeedViewModel,
|
reportsFeedViewModel,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
@@ -431,6 +450,7 @@ private fun RenderScreen(
|
|||||||
followersFeedViewModel,
|
followersFeedViewModel,
|
||||||
zapFeedViewModel,
|
zapFeedViewModel,
|
||||||
bookmarksFeedViewModel,
|
bookmarksFeedViewModel,
|
||||||
|
pinnedNotesFeedViewModel,
|
||||||
galleryFeedViewModel,
|
galleryFeedViewModel,
|
||||||
reportsFeedViewModel,
|
reportsFeedViewModel,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
@@ -451,6 +471,7 @@ private fun CreateAndRenderPages(
|
|||||||
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
||||||
zapFeedViewModel: UserProfileZapsViewModel,
|
zapFeedViewModel: UserProfileZapsViewModel,
|
||||||
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
||||||
|
pinnedNotesFeedViewModel: UserProfilePinnedNotesFeedViewModel,
|
||||||
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
||||||
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
@@ -472,9 +493,10 @@ private fun CreateAndRenderPages(
|
|||||||
5 -> TabFollowers(followersFeedViewModel, accountViewModel, nav)
|
5 -> TabFollowers(followersFeedViewModel, accountViewModel, nav)
|
||||||
6 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
|
6 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
|
||||||
7 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav)
|
7 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav)
|
||||||
8 -> TabFollowedTags(baseUser, accountViewModel, nav)
|
8 -> TabPinnedNotes(pinnedNotesFeedViewModel, accountViewModel, nav)
|
||||||
9 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav)
|
9 -> TabFollowedTags(baseUser, accountViewModel, nav)
|
||||||
10 -> TabRelays(baseUser, accountViewModel, nav)
|
10 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav)
|
||||||
|
11 -> TabRelays(baseUser, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,6 +526,7 @@ private fun CreateAndRenderTabs(
|
|||||||
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
||||||
zapFeedViewModel: UserProfileZapsViewModel,
|
zapFeedViewModel: UserProfileZapsViewModel,
|
||||||
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
||||||
|
pinnedNotesFeedViewModel: UserProfilePinnedNotesFeedViewModel,
|
||||||
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
||||||
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
@@ -520,6 +543,7 @@ private fun CreateAndRenderTabs(
|
|||||||
{ FollowersTabHeader(baseUser, followersFeedViewModel, accountViewModel) },
|
{ FollowersTabHeader(baseUser, followersFeedViewModel, accountViewModel) },
|
||||||
{ ZapTabHeader(zapFeedViewModel, accountViewModel) },
|
{ ZapTabHeader(zapFeedViewModel, accountViewModel) },
|
||||||
{ BookmarkTabHeader(baseUser, accountViewModel) },
|
{ BookmarkTabHeader(baseUser, accountViewModel) },
|
||||||
|
{ PinnedNotesTabHeader(baseUser, accountViewModel) },
|
||||||
{ FollowedTagsTabHeader(baseUser, accountViewModel) },
|
{ FollowedTagsTabHeader(baseUser, accountViewModel) },
|
||||||
{ ReportsTabHeader(baseUser, reportsFeedViewModel, accountViewModel) },
|
{ ReportsTabHeader(baseUser, reportsFeedViewModel, accountViewModel) },
|
||||||
{ RelaysTabHeader(baseUser, accountViewModel) },
|
{ RelaysTabHeader(baseUser, accountViewModel) },
|
||||||
|
|||||||
+2
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||||
@@ -33,6 +34,7 @@ import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendatio
|
|||||||
val UserProfileListKinds =
|
val UserProfileListKinds =
|
||||||
listOf(
|
listOf(
|
||||||
BookmarkListEvent.KIND,
|
BookmarkListEvent.KIND,
|
||||||
|
PinListEvent.KIND,
|
||||||
PeopleListEvent.KIND,
|
PeopleListEvent.KIND,
|
||||||
FollowListEvent.KIND,
|
FollowListEvent.KIND,
|
||||||
HashtagListEvent.KIND,
|
HashtagListEvent.KIND,
|
||||||
|
|||||||
+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.profile.pinnedNotes
|
||||||
|
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPinnedNotesCount
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PinnedNotesTabHeader(
|
||||||
|
baseUser: User,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val count by observeUserPinnedNotesCount(baseUser, accountViewModel)
|
||||||
|
|
||||||
|
Text(text = "$count ${stringRes(R.string.pinned_notes)}")
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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.profile.pinnedNotes
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TabPinnedNotes(
|
||||||
|
feedViewModel: UserProfilePinnedNotesFeedViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) { feedViewModel.invalidateData() }
|
||||||
|
|
||||||
|
Column(Modifier.fillMaxHeight()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(vertical = 0.dp),
|
||||||
|
) {
|
||||||
|
RefresheableFeedView(
|
||||||
|
feedViewModel,
|
||||||
|
null,
|
||||||
|
enablePullRefresh = false,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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.profile.pinnedNotes.dal
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||||
|
|
||||||
|
class UserProfilePinnedNotesFeedFilter(
|
||||||
|
val user: User,
|
||||||
|
val account: Account,
|
||||||
|
) : FeedFilter<Note>() {
|
||||||
|
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + user.pubkeyHex
|
||||||
|
|
||||||
|
override fun feed(): List<Note> {
|
||||||
|
val note = LocalCache.getOrCreateAddressableNote(PinListEvent.createPinAddress(user.pubkeyHex))
|
||||||
|
val noteEvent = note.event as? PinListEvent ?: return emptyList()
|
||||||
|
|
||||||
|
return noteEvent
|
||||||
|
.pinnedEvents()
|
||||||
|
.mapNotNull {
|
||||||
|
LocalCache.checkGetOrCreateNote(it.eventId)
|
||||||
|
}.reversed()
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.profile.pinnedNotes.dal
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class UserProfilePinnedNotesFeedViewModel(
|
||||||
|
val user: User,
|
||||||
|
val account: Account,
|
||||||
|
) : AndroidFeedViewModel(UserProfilePinnedNotesFeedFilter(user, account)) {
|
||||||
|
class Factory(
|
||||||
|
val user: User,
|
||||||
|
val account: Account,
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfilePinnedNotesFeedViewModel(user, account) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -418,6 +418,10 @@
|
|||||||
<string name="remove_from_private_bookmarks">Remove from Private Bookmarks</string>
|
<string name="remove_from_private_bookmarks">Remove from Private Bookmarks</string>
|
||||||
<string name="remove_from_public_bookmarks">Remove from Public Bookmarks</string>
|
<string name="remove_from_public_bookmarks">Remove from Public Bookmarks</string>
|
||||||
|
|
||||||
|
<string name="pinned_notes">Pinned Notes</string>
|
||||||
|
<string name="pin_to_profile">Pin to Profile</string>
|
||||||
|
<string name="unpin_from_profile">Unpin from Profile</string>
|
||||||
|
|
||||||
<string name="bookmark_lists">Bookmark Lists</string>
|
<string name="bookmark_lists">Bookmark Lists</string>
|
||||||
<string name="bookmark_list_icon_label">Icon for bookmark list</string>
|
<string name="bookmark_list_icon_label">Icon for bookmark list</string>
|
||||||
<string name="bookmark_list_creation_screen_title">New Bookmark List</string>
|
<string name="bookmark_list_creation_screen_title">New Bookmark List</string>
|
||||||
|
|||||||
@@ -21,10 +21,15 @@
|
|||||||
package com.vitorpamplona.quartz.nip51Lists
|
package com.vitorpamplona.quartz.nip51Lists
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.fastAny
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||||
|
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -35,23 +40,70 @@ class PinListEvent(
|
|||||||
tags: Array<Array<String>>,
|
tags: Array<Array<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey,
|
sig: HexKey,
|
||||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||||
fun pins() = tags.filter { it.size > 1 && it[0] == "pin" }.map { it[1] }
|
EventHintProvider {
|
||||||
|
override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint)
|
||||||
|
|
||||||
|
override fun linkedEventIds() = tags.mapNotNull(EventBookmark::parseId)
|
||||||
|
|
||||||
|
fun countPins() = tags.count(EventBookmark::isTagged)
|
||||||
|
|
||||||
|
fun pinnedEvents(): List<EventBookmark> = tags.mapNotNull(EventBookmark::parse)
|
||||||
|
|
||||||
|
fun isPinned(eventId: HexKey): Boolean = tags.any { EventBookmark.isTagged(it, eventId) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KIND = 33888
|
const val KIND = 10001
|
||||||
const val ALT = "Pinned Posts"
|
const val ALT = "Pinned Notes"
|
||||||
|
|
||||||
|
fun createPinAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
|
||||||
|
|
||||||
suspend fun create(
|
suspend fun create(
|
||||||
pins: List<String>,
|
pin: EventBookmark,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
): PinListEvent {
|
): PinListEvent {
|
||||||
val tags = mutableListOf<Array<String>>()
|
val tags = arrayOf(pin.toTagArray(), AltTag.assemble(ALT))
|
||||||
pins.forEach { tags.add(arrayOf("pin", it)) }
|
return signer.sign(createdAt, KIND, tags, "")
|
||||||
tags.add(AltTag.assemble(ALT))
|
}
|
||||||
|
|
||||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), "")
|
suspend fun add(
|
||||||
|
earlierVersion: PinListEvent,
|
||||||
|
pin: EventBookmark,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
): PinListEvent =
|
||||||
|
resign(
|
||||||
|
tags = earlierVersion.tags.plus(pin.toTagArray()),
|
||||||
|
signer = signer,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun remove(
|
||||||
|
earlierVersion: PinListEvent,
|
||||||
|
pin: EventBookmark,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
): PinListEvent =
|
||||||
|
resign(
|
||||||
|
tags = earlierVersion.tags.remove(pin.toTagIdOnly()),
|
||||||
|
signer = signer,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun resign(
|
||||||
|
tags: TagArray,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
): PinListEvent {
|
||||||
|
val newTags =
|
||||||
|
if (tags.fastAny(AltTag::match)) {
|
||||||
|
tags
|
||||||
|
} else {
|
||||||
|
tags + AltTag.assemble(ALT)
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer.sign(createdAt, KIND, newTags, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user