From 525b54e3c6cbfce6b537768204dd1b9c5c1b1acb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 10:39:32 +0000 Subject: [PATCH] feat: add attestation events to LocalCache, Home feed, profile feed, and NoteCompose UI - Load AttestationEvent (31871), AttestationRequestEvent (31872), AttestorRecommendationEvent (31873), and AttestorProficiencyEvent (11871) into LocalCache via consumeBaseReplaceable - Create beautiful card-based UI renderers for all four attestation types with color-coded status indicators, validity badges, and kind chips - Add attestation kinds to Home feed data source (FilterHomePostsByAuthors) and DAL filter (HomeNewThreadFeedFilter) - Add attestation kinds to user profile feed data source (FilterUserProfilePosts) and DAL filter (UserProfileNewThreadFeedFilter) - Add string resources for attestation status labels https://claude.ai/code/session_01BEMFoHZENBwzmzS6TMPxVE --- .../amethyst/model/LocalCache.kt | 32 ++ .../amethyst/ui/note/NoteCompose.kt | 24 + .../amethyst/ui/note/types/Attestation.kt | 429 ++++++++++++++++++ .../home/dal/HomeNewThreadFeedFilter.kt | 14 +- .../nip65Follows/FilterHomePostsByAuthors.kt | 8 + .../datasource/FilterUserProfilePosts.kt | 8 + .../dal/UserProfileNewThreadFeedFilter.kt | 10 +- amethyst/src/main/res/values/strings.xml | 18 + 8 files changed, 541 insertions(+), 2 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 6ddfc6fd9..d1c8d7e2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -39,6 +39,10 @@ import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.dateFormatter +import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent +import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent +import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent +import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent @@ -619,6 +623,30 @@ object LocalCache : ILocalCache, ICacheProvider { wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) + fun consume( + event: AttestationEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + + fun consume( + event: AttestationRequestEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + + fun consume( + event: AttestorRecommendationEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + + fun consume( + event: AttestorProficiencyEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + fun consumeRegularEvent( event: Event, relay: NormalizedRelayUrl?, @@ -3051,6 +3079,10 @@ object LocalCache : ILocalCache, ICacheProvider { is AppDefinitionEvent -> consume(event, relay, wasVerified) is AppRecommendationEvent -> consume(event, relay, wasVerified) is AppSpecificDataEvent -> consume(event, relay, wasVerified) + is AttestationEvent -> consume(event, relay, wasVerified) + is AttestationRequestEvent -> consume(event, relay, wasVerified) + is AttestorRecommendationEvent -> consume(event, relay, wasVerified) + is AttestorProficiencyEvent -> consume(event, relay, wasVerified) is AudioHeaderEvent -> consume(event, relay, wasVerified) is AudioTrackEvent -> consume(event, relay, wasVerified) is BadgeAwardEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 077683c28..4939c89b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -102,6 +102,10 @@ import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition +import com.vitorpamplona.amethyst.ui.note.types.RenderAttestation +import com.vitorpamplona.amethyst.ui.note.types.RenderAttestationRequest +import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorProficiency +import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorRecommendation import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward @@ -174,6 +178,10 @@ import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.replyModifier +import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent +import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent +import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent +import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward @@ -767,6 +775,22 @@ private fun RenderNoteRow( RenderAppDefinition(baseNote, accountViewModel, nav) } + is AttestationEvent -> { + RenderAttestation(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) + } + + is AttestationRequestEvent -> { + RenderAttestationRequest(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) + } + + is AttestorRecommendationEvent -> { + RenderAttestorRecommendation(baseNote, backgroundColor, accountViewModel, nav) + } + + is AttestorProficiencyEvent -> { + RenderAttestorProficiency(baseNote, backgroundColor, accountViewModel, nav) + } + is AudioTrackEvent -> { RenderAudioTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt new file mode 100644 index 000000000..302f99720 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt @@ -0,0 +1,429 @@ +/* + * 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.note.types + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.HourglassTop +import androidx.compose.material.icons.filled.Recommend +import androidx.compose.material.icons.filled.Send +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.filled.VerifiedUser +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember +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.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.replyModifier +import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent +import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.AttestationStatus +import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity +import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent +import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent +import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Composable +fun RenderAttestation( + note: Note, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? AttestationEvent ?: return + + val validity = remember(noteEvent) { noteEvent.validity() } + val status = remember(noteEvent) { noteEvent.status() } + val validFrom = remember(noteEvent) { noteEvent.validFrom() } + val validTo = remember(noteEvent) { noteEvent.validTo() } + val content = remember(noteEvent) { noteEvent.content.ifBlank { null } } + + val statusColor = remember(status, validity) { attestationColor(status, validity) } + val statusIcon = remember(status, validity) { attestationIcon(status, validity) } + val statusLabel = attestationStatusLabel(status, validity) + + Column( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .border(1.dp, statusColor.copy(alpha = 0.3f), RoundedCornerShape(12.dp)) + .background(statusColor.copy(alpha = 0.06f)) + .padding(12.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = statusIcon, + contentDescription = stringRes(R.string.attestation), + tint = statusColor, + modifier = Modifier.size(24.dp), + ) + Text( + text = statusLabel, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = statusColor, + ) + } + + if (validFrom != null || validTo != null) { + Spacer(modifier = Modifier.height(6.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + validFrom?.let { + Text( + text = stringRes(R.string.attestation_valid_from, formatTimestamp(it)), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + validTo?.let { + Text( + text = stringRes(R.string.attestation_valid_to, formatTimestamp(it)), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + + content?.let { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + + if (quotesLeft > 0) { + note.replyTo?.firstOrNull()?.let { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringRes(R.string.attestation_attests_to), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + NoteCompose( + baseNote = it, + modifier = MaterialTheme.colorScheme.replyModifier, + isQuotedNote = true, + unPackReply = ReplyRenderType.NONE, + makeItShort = true, + quotesLeft = quotesLeft - 1, + parentBackgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} + +@Composable +fun RenderAttestationRequest( + note: Note, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? AttestationRequestEvent ?: return + val content = remember(noteEvent) { noteEvent.content.ifBlank { null } } + + Column( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .border( + 1.dp, + MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), + RoundedCornerShape(12.dp), + ).background(MaterialTheme.colorScheme.primary.copy(alpha = 0.06f)) + .padding(12.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Send, + contentDescription = stringRes(R.string.attestation_request), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp), + ) + Text( + text = stringRes(R.string.attestation_request), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + ) + } + + content?.let { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + + if (quotesLeft > 0) { + note.replyTo?.firstOrNull()?.let { + Spacer(modifier = Modifier.height(8.dp)) + NoteCompose( + baseNote = it, + modifier = MaterialTheme.colorScheme.replyModifier, + isQuotedNote = true, + unPackReply = ReplyRenderType.NONE, + makeItShort = true, + quotesLeft = quotesLeft - 1, + parentBackgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun RenderAttestorRecommendation( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? AttestorRecommendationEvent ?: return + val kinds = remember(noteEvent) { noteEvent.kinds() } + val description = remember(noteEvent) { noteEvent.description() } + + Column( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .border( + 1.dp, + MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f), + RoundedCornerShape(12.dp), + ).background(MaterialTheme.colorScheme.tertiary.copy(alpha = 0.06f)) + .padding(12.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Recommend, + contentDescription = stringRes(R.string.attestor_recommendation), + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(24.dp), + ) + Text( + text = stringRes(R.string.attestor_recommendation), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.tertiary, + ) + } + + if (kinds.isNotEmpty()) { + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = + stringRes( + R.string.attestor_recommendation_for_kinds, + kinds.joinToString(", "), + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + description?.let { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun RenderAttestorProficiency( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? AttestorProficiencyEvent ?: return + val kinds = remember(noteEvent) { noteEvent.kinds() } + val description = remember(noteEvent) { noteEvent.description() } + + Column( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .border( + 1.dp, + MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f), + RoundedCornerShape(12.dp), + ).background(MaterialTheme.colorScheme.secondary.copy(alpha = 0.06f)) + .padding(12.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Star, + contentDescription = stringRes(R.string.attestor_proficiency), + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(24.dp), + ) + Text( + text = stringRes(R.string.attestor_proficiency), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.secondary, + ) + } + + if (kinds.isNotEmpty()) { + Spacer(modifier = Modifier.height(6.dp)) + FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + kinds.forEach { kind -> + Text( + text = "Kind $kind", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.secondary, + modifier = + Modifier + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f)) + .padding(horizontal = 6.dp, vertical = 2.dp), + ) + } + } + } + + description?.let { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +private fun attestationColor( + status: AttestationStatus?, + validity: Validity?, +): Color = + when { + status == AttestationStatus.REVOKED -> Color(0xFFB71C1C) + status == AttestationStatus.REJECTED -> Color(0xFFB71C1C) + validity == Validity.INVALID -> Color(0xFFB71C1C) + status == AttestationStatus.VERIFIED -> Color(0xFF2E7D32) + validity == Validity.VALID -> Color(0xFF2E7D32) + status == AttestationStatus.VERIFYING -> Color(0xFFF57F17) + status == AttestationStatus.ACCEPTED -> Color(0xFF1565C0) + else -> Color(0xFF757575) + } + +private fun attestationIcon( + status: AttestationStatus?, + validity: Validity?, +): ImageVector = + when { + status == AttestationStatus.REVOKED -> Icons.Default.Close + status == AttestationStatus.REJECTED -> Icons.Default.Close + validity == Validity.INVALID -> Icons.Default.Close + status == AttestationStatus.VERIFIED -> Icons.Default.VerifiedUser + validity == Validity.VALID -> Icons.Default.CheckCircle + status == AttestationStatus.VERIFYING -> Icons.Default.HourglassTop + status == AttestationStatus.ACCEPTED -> Icons.Default.CheckCircle + else -> Icons.Default.VerifiedUser + } + +@Composable +private fun attestationStatusLabel( + status: AttestationStatus?, + validity: Validity?, +): String = + when { + status == AttestationStatus.REVOKED -> stringRes(R.string.attestation_status_revoked) + status == AttestationStatus.REJECTED -> stringRes(R.string.attestation_status_rejected) + validity == Validity.INVALID -> stringRes(R.string.attestation_invalid) + status == AttestationStatus.VERIFIED -> stringRes(R.string.attestation_status_verified) + validity == Validity.VALID -> stringRes(R.string.attestation_valid) + status == AttestationStatus.VERIFYING -> stringRes(R.string.attestation_status_verifying) + status == AttestationStatus.ACCEPTED -> stringRes(R.string.attestation_status_accepted) + else -> stringRes(R.string.attestation) + } + +private fun formatTimestamp(timestamp: Long): String { + val sdf = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()) + return sdf.format(Date(timestamp * 1000)) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index 66f61eca9..2e009ce32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -29,6 +29,10 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthors 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.attestations.attestation.AttestationEvent +import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent +import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent +import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent @@ -60,6 +64,10 @@ class HomeNewThreadFeedFilter( LongTextNoteEvent.KIND, LiveChessGameChallengeEvent.KIND, LiveChessGameEndEvent.KIND, + AttestationEvent.KIND, + AttestationRequestEvent.KIND, + AttestorRecommendationEvent.KIND, + AttestorProficiencyEvent.KIND, ) } @@ -126,7 +134,11 @@ class HomeNewThreadFeedFilter( noteEvent is AudioHeaderEvent || noteEvent is ChessGameEvent || noteEvent is LiveChessGameChallengeEvent || - noteEvent is LiveChessGameEndEvent + noteEvent is LiveChessGameEndEvent || + noteEvent is AttestationEvent || + noteEvent is AttestationRequestEvent || + noteEvent is AttestorRecommendationEvent || + noteEvent is AttestorProficiencyEvent ) && filterParams.match(noteEvent, it.relays) && it.isNewThread() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt index 0aa7d3a1a..4275bfae2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -23,6 +23,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follo import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent +import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent +import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent +import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent @@ -66,6 +70,10 @@ val HomePostsNewThreadKinds = ChessGameEvent.KIND, LiveChessGameChallengeEvent.KIND, LiveChessGameEndEvent.KIND, + AttestationEvent.KIND, + AttestationRequestEvent.KIND, + AttestorRecommendationEvent.KIND, + AttestorProficiencyEvent.KIND, ) val HomePostsConversationKinds = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt index a77b676b6..90a54e478 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt @@ -23,6 +23,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent +import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent +import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent +import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent @@ -66,6 +70,10 @@ val UserProfilePostKinds2 = VoiceReplyEvent.KIND, ZapPollEvent.KIND, PinListEvent.KIND, + AttestationEvent.KIND, + AttestationRequestEvent.KIND, + AttestorRecommendationEvent.KIND, + AttestorProficiencyEvent.KIND, ) fun filterUserProfilePosts( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt index ea7964f1a..bdaa1d8af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt @@ -27,6 +27,10 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder +import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent +import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent +import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent +import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent @@ -87,7 +91,11 @@ class UserProfileNewThreadFeedFilter( it.event is AudioTrackEvent || it.event is AudioHeaderEvent || it.event is VoiceEvent || - it.event is TorrentEvent + it.event is TorrentEvent || + it.event is AttestationEvent || + it.event is AttestationRequestEvent || + it.event is AttestorRecommendationEvent || + it.event is AttestorProficiencyEvent ) && it.isNewThread() && account.isAcceptable(it) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index a93534129..cd3a536a5 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1873,4 +1873,22 @@ Downloading Error Completed + + Attestation + Valid + Invalid + Accepted + Rejected + Verifying + Verified + Revoked + Valid from %1$s + Valid to %1$s + Attestation Request + Requesting attestation for an event + Attestor Recommendation + Recommended for kinds: %1$s + Attestor Proficiency + Proficient in verifying kinds: %1$s + Attests to