feat: add CodeSnippetEvent (NIP-C0) rendering to NoteCompose and ThreadFeedView
- Create CodeSnippet.kt with RenderCodeSnippetEvent (feed card view) and RenderCodeSnippetHeaderForThread (full thread view) - Feed card shows: filename/language badge, description, 5-line code preview in a monospace box with rounded border - Thread header shows: full code in scrollable monospace block plus metadata (license, runtime, deps) - Add is CodeSnippetEvent branch to NoteCompose.kt RenderNoteRow when-statement - Add is CodeSnippetEvent branch to ThreadFeedView.kt header when-statement https://claude.ai/code/session_01X6jjsMLGBUmcaWXHJKmrEn
This commit is contained in:
@@ -101,6 +101,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCodeSnippetEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessageEncryptedFile
|
||||
@@ -242,6 +243,7 @@ import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
|
||||
import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -790,6 +792,10 @@ private fun RenderNoteRow(
|
||||
RenderLongFormContent(baseNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is CodeSnippetEvent -> {
|
||||
RenderCodeSnippetEvent(baseNote)
|
||||
}
|
||||
|
||||
is WikiNoteEvent -> {
|
||||
RenderWikiContent(baseNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent
|
||||
|
||||
@Composable
|
||||
fun RenderCodeSnippetEvent(
|
||||
baseNote: Note,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? CodeSnippetEvent ?: return
|
||||
|
||||
val name =
|
||||
remember(noteEvent) {
|
||||
noteEvent.snippetName()
|
||||
?: noteEvent.language()?.let { "$it snippet" }
|
||||
?: "Code Snippet"
|
||||
}
|
||||
val language = remember(noteEvent) { noteEvent.language() ?: noteEvent.extension() }
|
||||
val description = remember(noteEvent) { noteEvent.snippetDescription() }
|
||||
val codePreview = remember(noteEvent) { noteEvent.content.lines().take(5).joinToString("\n") }
|
||||
|
||||
Column(MaterialTheme.colorScheme.replyModifier) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
language?.let {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(QuoteBorder)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description?.ifBlank { null }?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
.clip(QuoteBorder)
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f))
|
||||
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), QuoteBorder)
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Text(
|
||||
text = codePreview,
|
||||
style =
|
||||
TextStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
),
|
||||
maxLines = 5,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderCodeSnippetHeaderForThread(
|
||||
noteEvent: CodeSnippetEvent,
|
||||
) {
|
||||
val name =
|
||||
remember(noteEvent) {
|
||||
noteEvent.snippetName()
|
||||
?: noteEvent.language()?.let { "$it snippet" }
|
||||
?: "Code Snippet"
|
||||
}
|
||||
val language = remember(noteEvent) { noteEvent.language() ?: noteEvent.extension() }
|
||||
val description = remember(noteEvent) { noteEvent.snippetDescription() }
|
||||
val license = remember(noteEvent) { noteEvent.license() }
|
||||
val runtime = remember(noteEvent) { noteEvent.runtime() }
|
||||
val deps = remember(noteEvent) { noteEvent.deps() }
|
||||
|
||||
Column(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
language?.let {
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(QuoteBorder)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description?.ifBlank { null }?.let {
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(QuoteBorder)
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f))
|
||||
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), QuoteBorder)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = noteEvent.content,
|
||||
style =
|
||||
TextStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 13.sp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
license?.let {
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
Text(
|
||||
text = "License: $it",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
runtime?.let {
|
||||
Text(
|
||||
text = "Runtime: $it",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
if (deps.isNotEmpty()) {
|
||||
Text(
|
||||
text = "Deps: ${deps.joinToString(", ")}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -142,6 +142,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessageEncryptedFile
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCodeSnippetHeaderForThread
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
|
||||
@@ -248,6 +249,7 @@ import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
|
||||
import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -532,6 +534,7 @@ private fun FullBleedNoteCompose(
|
||||
is LongTextNoteEvent -> RenderLongFormHeaderForThread(noteEvent, baseNote, accountViewModel)
|
||||
is WikiNoteEvent -> RenderWikiHeaderForThread(noteEvent, accountViewModel, nav)
|
||||
is ClassifiedsEvent -> RenderClassifiedsReaderForThread(noteEvent, baseNote, accountViewModel, nav)
|
||||
is CodeSnippetEvent -> RenderCodeSnippetHeaderForThread(noteEvent)
|
||||
}
|
||||
|
||||
Row(
|
||||
|
||||
Reference in New Issue
Block a user