From 7c21f077db8884c75286651c3d97af20a5fab226 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 15 May 2023 22:18:12 -0400 Subject: [PATCH] Support for PinStr (33888) --- .../amethyst/model/LocalCache.kt | 13 ++ .../amethyst/service/NostrGlobalDataSource.kt | 3 +- .../amethyst/service/NostrHomeDataSource.kt | 5 +- .../service/NostrSingleEventDataSource.kt | 7 +- .../service/NostrUserProfileDataSource.kt | 2 +- .../amethyst/service/model/Event.kt | 1 + .../amethyst/service/model/PinListEvent.kt | 41 ++++++ .../amethyst/ui/note/NoteCompose.kt | 126 +++++++++++++++++- .../amethyst/ui/screen/ThreadFeedView.kt | 3 + 9 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index ed3e9d0cf..4973cac35 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -281,6 +281,18 @@ object LocalCache { refreshObservers(note) } + private fun consume(event: PinListEvent) { + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + // Already processed this event. + if (note.event != null) return + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + private fun consume(event: AudioTrackEvent) { val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) @@ -961,6 +973,7 @@ object LocalCache { is LongTextNoteEvent -> consume(event, relay) is MetadataEvent -> consume(event) is PrivateDmEvent -> consume(event, relay) + is PinListEvent -> consume(event) is PeopleListEvent -> consume(event) is ReactionEvent -> consume(event) is RecommendRelayEvent -> consume(event) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt index 735036304..cec80961f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt @@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent +import com.vitorpamplona.amethyst.service.model.PinListEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType @@ -14,7 +15,7 @@ object NostrGlobalDataSource : NostrDataSource("GlobalFeed") { fun createGlobalFilter() = TypedFilter( types = setOf(FeedType.GLOBAL), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, PollNoteEvent.kind, ChannelMessageEvent.kind, AudioTrackEvent.kind, LongTextNoteEvent.kind, HighlightEvent.kind), + kinds = listOf(TextNoteEvent.kind, PollNoteEvent.kind, ChannelMessageEvent.kind, AudioTrackEvent.kind, PinListEvent.kind, LongTextNoteEvent.kind, HighlightEvent.kind), limit = 200 ) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index a4277b576..850b37069 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent +import com.vitorpamplona.amethyst.service.model.PinListEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.EOSEAccount @@ -57,7 +58,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { return TypedFilter( types = setOf(FeedType.FOLLOWS), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind), + kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind), authors = followSet, limit = 400, since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList @@ -73,7 +74,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { return TypedFilter( types = setOf(FeedType.FOLLOWS), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind), + kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind), tags = mapOf( "t" to hashToLoad.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 41edd2886..3fce56a25 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -29,7 +29,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind, BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind, - PollNoteEvent.kind, AudioTrackEvent.kind + PollNoteEvent.kind, AudioTrackEvent.kind, PinListEvent.kind ), tags = mapOf("a" to listOf(aTag.toTag())), since = it.lastReactionsDownloadTime @@ -81,7 +81,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { LnZapRequestEvent.kind, PollNoteEvent.kind, HighlightEvent.kind, - AudioTrackEvent.kind + AudioTrackEvent.kind, + PinListEvent.kind ), tags = mapOf("e" to listOf(it.idHex)), since = it.lastReactionsDownloadTime @@ -120,7 +121,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind, PrivateDmEvent.kind, FileHeaderEvent.kind, FileStorageEvent.kind, FileStorageHeaderEvent.kind, - HighlightEvent.kind, AudioTrackEvent.kind + HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind ), ids = interestedEvents.toList() ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 2246c7c80..5b9df5826 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -28,7 +28,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") { TypedFilter( types = COMMON_FEED_TYPES, filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, RepostEvent.kind, LongTextNoteEvent.kind, AudioTrackEvent.kind, PollNoteEvent.kind, HighlightEvent.kind), + kinds = listOf(TextNoteEvent.kind, RepostEvent.kind, LongTextNoteEvent.kind, AudioTrackEvent.kind, PinListEvent.kind, PollNoteEvent.kind, HighlightEvent.kind), authors = listOf(it.pubkeyHex), limit = 200 ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 38a99ac76..91b825e8d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -244,6 +244,7 @@ open class Event( LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) PeopleListEvent.kind -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig) + PinListEvent.kind -> PinListEvent(id, pubKey, createdAt, tags, content, sig) PollNoteEvent.kind -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig) PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt new file mode 100644 index 000000000..100aaf981 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt @@ -0,0 +1,41 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import nostr.postr.Utils +import java.util.Date + +class PinListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { + + override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" + override fun address() = ATag(kind, pubKey, dTag(), null) + + fun pins() = tags.filter { it.size > 1 && it[0] == "pin" }.map { it[1] } + + companion object { + const val kind = 33888 + + fun create( + pins: List, + privateKey: ByteArray, + createdAt: Long = Date().time / 1000 + ): PinListEvent { + val tags = mutableListOf>() + pins.forEach { + tags.add(listOf("pin", it)) + } + + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, "") + val sig = Utils.sign(id, privateKey) + return PinListEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index c93ff6bec..e660f9569 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -40,6 +40,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -51,6 +52,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush @@ -66,7 +68,6 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -101,6 +102,7 @@ import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.Participant import com.vitorpamplona.amethyst.service.model.PeopleListEvent +import com.vitorpamplona.amethyst.service.model.PinListEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent @@ -385,6 +387,10 @@ fun NoteComposeInner( RenderAudioTrack(note, loggedIn, accountViewModel, navController) } + is PinListEvent -> { + RenderPinListEvent(noteState, backgroundColor, accountViewModel, navController) + } + is PrivateDmEvent -> { RenderPrivateMessage(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, navController) } @@ -877,6 +883,116 @@ private fun RenderRepost( } } +@Composable +private fun RenderPinListEvent( + noteState: NoteState?, + backgroundColor: Color, + accountViewModel: AccountViewModel, + navController: NavController +) { + val noteEvent = noteState?.note?.event as? PinListEvent ?: return + + PinListHeader(noteState, backgroundColor, accountViewModel, navController) + + ReactionsRow(noteState?.note, accountViewModel, navController) + + Divider( + modifier = Modifier.padding(top = 10.dp), + thickness = 0.25.dp + ) +} + +@Composable +fun PinListHeader( + noteState: NoteState?, + backgroundColor: Color, + accountViewModel: AccountViewModel, + navController: NavController +) { + val note = remember(noteState) { noteState?.note } ?: return + val noteEvent = note.event as? PinListEvent ?: return + + var pins by remember { mutableStateOf>(noteEvent.pins()) } + + var expanded by remember { + mutableStateOf(false) + } + + val pinsToShow = if (expanded) { + pins + } else { + pins.take(3) + } + + Text( + text = "#${noteEvent.dTag()}", + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .padding(5.dp), + textAlign = TextAlign.Center + ) + + Box { + FlowRow(modifier = Modifier.padding(top = 5.dp)) { + pinsToShow.forEach { pin -> + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = CenterVertically) { + Icon( + imageVector = Icons.Default.PushPin, + contentDescription = null, + tint = MaterialTheme.colors.onBackground.copy(0.12f), + modifier = Modifier.size(15.dp) + ) + + Spacer(modifier = Modifier.width(5.dp)) + + TranslatableRichTextViewer( + content = pin, + canPreview = true, + tags = emptyList(), + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + navController = navController + ) + } + } + } + + if (pins.size > 3 && !expanded) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf( + backgroundColor.copy(alpha = 0f), + backgroundColor + ) + ) + ) + ) { + Button( + modifier = Modifier.padding(top = 10.dp), + onClick = { expanded = !expanded }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.primary.copy(alpha = 0.32f) + .compositeOver(MaterialTheme.colors.background) + ), + contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp) + ) { + Text(text = stringResource(R.string.show_more), color = Color.White) + } + } + } + } +} + @Composable private fun RenderAudioTrack( note: Note, @@ -1618,9 +1734,11 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, loggedIn: User, nav participantUsers.forEach { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 5.dp, start = 10.dp, end = 10.dp).clickable { - navController.navigate("User/${it.second.pubkeyHex}") - } + modifier = Modifier + .padding(top = 5.dp, start = 10.dp, end = 10.dp) + .clickable { + navController.navigate("User/${it.second.pubkeyHex}") + } ) { UserPicture(it.second, loggedIn, 25.dp) Spacer(Modifier.width(5.dp)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index b0909124e..cc1d47a35 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.PeopleListEvent +import com.vitorpamplona.amethyst.service.model.PinListEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer @@ -355,6 +356,8 @@ fun NoteMaster( DisplayPeopleList(noteState, MaterialTheme.colors.background, accountViewModel, navController) } else if (noteEvent is AudioTrackEvent) { AudioTrackHeader(noteEvent, note, account.userProfile(), navController) + } else if (noteEvent is PinListEvent) { + PinListHeader(noteState, MaterialTheme.colors.background, accountViewModel, navController) } else if (noteEvent is HighlightEvent) { DisplayHighlight( noteEvent.quote(),