Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import work.socialhub.planetlink.action.callback.comment.UpdateCommentCallback
import work.socialhub.planetlink.action.callback.lifecycle.ConnectCallback
import work.socialhub.planetlink.action.callback.lifecycle.DisconnectCallback
import work.socialhub.planetlink.action.callback.lifecycle.ErrorCallback
import work.socialhub.planetlink.model.common.AttributedString
import work.socialhub.planetlink.define.ServiceType
import work.socialhub.planetlink.model.Comment
import work.socialhub.planetlink.model.CommentUpdateStream
Expand Down Expand Up @@ -222,6 +223,9 @@ internal class NostrCommentUpdateStream(
}
if (quotedEventId == note.event.id) {
sharedComment = NostrMapper.comment(note, service, userMe)
text = text?.displayText?.let { content ->
AttributedString.plain(NostrMapper.stripQuoteReference(content, note.event.id))
}
return true
}
return (sharedComment as? NostrComment)?.applyNote(note) == true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import work.socialhub.knostr.social.model.NostrNote
import work.socialhub.knostr.social.model.NostrReaction
import work.socialhub.knostr.social.model.NostrRelationship
import work.socialhub.knostr.social.model.NostrUser as KnostrUser
import work.socialhub.knostr.util.Nip21
import work.socialhub.planetlink.define.MediaType
import work.socialhub.planetlink.model.Comment
import work.socialhub.planetlink.model.ID
Expand All @@ -25,6 +26,9 @@ import work.socialhub.planetlink.nostr.model.NostrUser
/** Nostr エンティティのマッピング */
object NostrMapper {

private val NOSTR_EVENT_REFERENCE =
Regex("nostr:(?:note|nevent)1[ac-hj-np-z02-9]+", RegexOption.IGNORE_CASE)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
/** ユーザーマッピング */
fun user(
knostrUser: KnostrUser,
Expand Down Expand Up @@ -184,7 +188,7 @@ object NostrMapper {
.map { it[1].lowercase() }
.toSet()

val attributed = AttributedString.plain(note.content)
val attributed = AttributedString.plain(displayContent(note))

val validated = attributed.elements.map { elem ->
if (elem.kind == AttributedKind.HASH_TAG && elem is AttributedItem) {
Expand All @@ -199,4 +203,24 @@ object NostrMapper {
}
return AttributedString(validated)
}

private fun displayContent(note: NostrNote): String {
val quotedEventId = note.quotedNote?.event?.id ?: return note.content

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip the reference after deferred quote resolution

When the quoted note is not cached, this returns the original content because quotedNote is null. The later resolution path in NostrCommentUpdateStream.applyNote (lines 223–225) only assigns sharedComment and never remaps the parent text, so the emitted update permanently displays both the raw nostr: reference and the resolved embedded quote. The stripping logic must also run when that deferred quote is attached.

Useful? React with 👍 / 👎.

return stripQuoteReference(note.content, quotedEventId)
}

internal fun stripQuoteReference(
content: String,
quotedEventId: String,
): String {
val stripped = NOSTR_EVENT_REFERENCE.replace(content) { match ->
val referencedEventId = Nip21.extractEventIds(match.value.lowercase()).singleOrNull()
if (referencedEventId.equals(quotedEventId, ignoreCase = true)) {
""
} else {
match.value
}
}
return if (stripped == content) content else stripped.trimEnd()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package work.socialhub.planetlink.nostr.action

import work.socialhub.knostr.entity.NostrEvent
import work.socialhub.knostr.social.model.NostrNote
import work.socialhub.knostr.util.Bech32
import work.socialhub.knostr.util.Hex
import work.socialhub.planetlink.model.Account
import work.socialhub.planetlink.model.Service
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

class NostrMapperTest {

@Test
fun removesResolvedQuoteReferenceFromDisplayText() {
val quotedEventId = "22".repeat(32)
val quotedNote = note(
eventId = quotedEventId,
content = "Quoted content",
)
val reference = nevent(quotedEventId)
val source = note(
eventId = "11".repeat(32),
content = "Additional comment\n\nnostr:$reference",
).apply {
this.quotedEventId = quotedEventId
this.quotedNote = quotedNote
}

val comment = NostrMapper.comment(source, service())

assertEquals("Additional comment", comment.text?.displayText)
assertNotNull(comment.sharedComment)
}

@Test
fun preservesReferenceToAnotherEvent() {
val quotedEventId = "22".repeat(32)
val otherEventId = "33".repeat(32)
val otherReference = nevent(otherEventId)
val source = note(
eventId = "11".repeat(32),
content = "See also nostr:$otherReference",
).apply {
this.quotedEventId = quotedEventId
this.quotedNote = note(quotedEventId, "Quoted content")
}

val comment = NostrMapper.comment(source, service())

assertEquals("See also nostr:$otherReference", comment.text?.displayText)
}

@Test
fun removesResolvedNote1QuoteReferenceFromDisplayText() {
val quotedEventId = "22".repeat(32)
val quotedNote = note(
eventId = quotedEventId,
content = "Quoted content",
)
val reference = note1(quotedEventId)
val source = note(
eventId = "11".repeat(32),
content = "Additional comment\n\nnostr:$reference",
).apply {
this.quotedEventId = quotedEventId
this.quotedNote = quotedNote
}

val comment = NostrMapper.comment(source, service())

assertEquals("Additional comment", comment.text?.displayText)
assertNotNull(comment.sharedComment)
}

@Test
fun removesResolvedUppercaseQuoteReferenceFromDisplayText() {
val quotedEventId = "22".repeat(32)
val quotedNote = note(
eventId = quotedEventId,
content = "Quoted content",
)
val reference = nevent(quotedEventId).uppercase()
val source = note(
eventId = "11".repeat(32),
content = "Additional comment\n\nnostr:$reference",
).apply {
this.quotedEventId = quotedEventId
this.quotedNote = quotedNote
}

val comment = NostrMapper.comment(source, service())

assertEquals("Additional comment", comment.text?.displayText)
assertNotNull(comment.sharedComment)
}

@Test
fun preservesQuoteReferenceUntilQuoteIsResolved() {
val quotedEventId = "22".repeat(32)
val reference = nevent(quotedEventId)
val source = note(
eventId = "11".repeat(32),
content = "Additional comment\n\nnostr:$reference",
).apply {
this.quotedEventId = quotedEventId
}

val comment = NostrMapper.comment(source, service())

assertEquals(
"Additional comment\n\nnostr:$reference",
comment.text?.displayText,
)
}

private fun note(
eventId: String,
content: String,
): NostrNote {
return NostrNote().apply {
event = NostrEvent(
id = eventId,
pubkey = "44".repeat(32),
createdAt = 1_000,
kind = 1,
tags = emptyList(),
content = content,
sig = "55".repeat(64),
)
this.content = content
createdAt = event.createdAt
noteId = Bech32.encode("note", Hex.decode(eventId))
}
}

private fun nevent(eventId: String): String {
val tlv = byteArrayOf(0, 32) + Hex.decode(eventId)
return Bech32.encode("nevent", tlv)
}

private fun note1(eventId: String): String {
return Bech32.encode("note", Hex.decode(eventId))
}

private fun service(): Service {
val account = Account()
return Service("nostr", account).also {
account.service = it
}
}
}
Loading