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
This commit is contained in:
Claude
2026-03-20 10:39:32 +00:00
parent 1159edad15
commit 525b54e3c6
8 changed files with 541 additions and 2 deletions
@@ -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)
@@ -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)
}
@@ -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<Color>,
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<Color>,
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<Color>,
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<Color>,
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))
}
@@ -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()
@@ -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 =
@@ -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(
@@ -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)
+18
View File
@@ -1873,4 +1873,22 @@
<string name="event_sync_status_downloading">Downloading</string>
<string name="event_sync_status_error">Error</string>
<string name="event_sync_status_completed">Completed</string>
<string name="attestation">Attestation</string>
<string name="attestation_valid">Valid</string>
<string name="attestation_invalid">Invalid</string>
<string name="attestation_status_accepted">Accepted</string>
<string name="attestation_status_rejected">Rejected</string>
<string name="attestation_status_verifying">Verifying</string>
<string name="attestation_status_verified">Verified</string>
<string name="attestation_status_revoked">Revoked</string>
<string name="attestation_valid_from">Valid from %1$s</string>
<string name="attestation_valid_to">Valid to %1$s</string>
<string name="attestation_request">Attestation Request</string>
<string name="attestation_request_description">Requesting attestation for an event</string>
<string name="attestor_recommendation">Attestor Recommendation</string>
<string name="attestor_recommendation_for_kinds">Recommended for kinds: %1$s</string>
<string name="attestor_proficiency">Attestor Proficiency</string>
<string name="attestor_proficiency_for_kinds">Proficient in verifying kinds: %1$s</string>
<string name="attestation_attests_to">Attests to</string>
</resources>