Merge pull request #2891 from vitorpamplona/claude/comment-reply-context-XMBOV
Display external identifier scopes in NIP-22 comments
This commit is contained in:
+93
-1
@@ -20,12 +20,37 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.nip22Comments
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
|
||||
@Composable
|
||||
fun DisplayExternalId(
|
||||
@@ -42,6 +67,73 @@ fun DisplayExternalId(
|
||||
DisplayHashtagExternalId(externalId, accountViewModel, nav)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
is UrlId -> {
|
||||
DisplayUrlExternalId(externalId)
|
||||
}
|
||||
|
||||
else -> {
|
||||
DisplayGenericExternalId(externalId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayUrlExternalId(externalId: UrlId) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
DisplayExternalIdChip(
|
||||
symbol = MaterialSymbols.Link,
|
||||
contentDescription = stringRes(id = R.string.external_url_scope),
|
||||
label = externalId.url,
|
||||
linkInteractionListener = { runCatching { uriHandler.openUri(externalId.url) } },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayGenericExternalId(externalId: ExternalId) {
|
||||
DisplayExternalIdChip(
|
||||
symbol = MaterialSymbols.Public,
|
||||
contentDescription = stringRes(id = R.string.external_id_scope),
|
||||
label = externalId.toScope(),
|
||||
linkInteractionListener = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayExternalIdChip(
|
||||
symbol: MaterialSymbol,
|
||||
contentDescription: String,
|
||||
label: String,
|
||||
linkInteractionListener: LinkInteractionListener?,
|
||||
) {
|
||||
Row(modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
symbol = symbol,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
Spacer(StdHorzSpacer)
|
||||
|
||||
Text(
|
||||
text =
|
||||
if (linkInteractionListener != null) {
|
||||
buildAnnotatedString {
|
||||
withLink(
|
||||
LinkAnnotation.Clickable("externalId", null, linkInteractionListener),
|
||||
) {
|
||||
append(label)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buildAnnotatedString { append(label) }
|
||||
},
|
||||
style =
|
||||
LocalTextStyle.current.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent
|
||||
import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition
|
||||
import com.vitorpamplona.amethyst.ui.note.ReplyToLabel
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.note.nip22Comments.DisplayExternalId
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
@@ -54,7 +55,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.scope
|
||||
|
||||
enum class ReplyRenderType {
|
||||
FULL,
|
||||
@@ -119,6 +122,14 @@ fun RenderTextEvent(
|
||||
Spacer(modifier = HalfVertSpacer)
|
||||
}
|
||||
}
|
||||
} else if (!makeItShort && noteEvent is CommentEvent) {
|
||||
// A comment scoped to an external identifier (`I` tag) has no in-cache parent
|
||||
// note. Show the scope itself as the reply context.
|
||||
val scope = remember(note) { noteEvent.scope() }
|
||||
if (scope != null) {
|
||||
DisplayExternalId(scope, accountViewModel, nav)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1475,6 +1475,9 @@
|
||||
<string name="hashtag_exclusive">Hashtag-exclusive Post</string>
|
||||
<string name="hashtag_exclusive_explainer">Only followers of the hashtag will see it. Your general followers won\'t see it.</string>
|
||||
|
||||
<string name="external_url_scope">Comment on a website</string>
|
||||
<string name="external_id_scope">Comment on an external resource</string>
|
||||
|
||||
<string name="long_form_reading_minutes">%1$d min read</string>
|
||||
|
||||
|
||||
|
||||
@@ -773,15 +773,20 @@ open class Note(
|
||||
return author?.reportsOrNull()?.hasReportNewerThan(dayAgo) ?: false
|
||||
}
|
||||
|
||||
fun isNewThread(): Boolean =
|
||||
(
|
||||
fun isNewThread(): Boolean {
|
||||
val event = event
|
||||
return (
|
||||
event is RepostEvent ||
|
||||
event is GenericRepostEvent ||
|
||||
replyTo == null ||
|
||||
replyTo?.size == 0
|
||||
) &&
|
||||
// A comment scoped to an external identifier (`I` tag) is a reply to that
|
||||
// scope, even though there is no in-cache parent note to populate replyTo.
|
||||
!(event is CommentEvent && event.hasRootScopeIdentifier()) &&
|
||||
event !is ChannelMessageEvent &&
|
||||
event !is LiveActivitiesChatMessageEvent
|
||||
}
|
||||
|
||||
fun hasZapped(loggedIn: User): Boolean = zaps.any { it.key.author == loggedIn }
|
||||
|
||||
|
||||
@@ -181,6 +181,9 @@ class CommentEvent(
|
||||
|
||||
fun isScoped(scopeTest: (String) -> Boolean) = tags.any { RootIdentifierTag.isTagged(it, scopeTest) || ReplyIdentifierTag.isTagged(it, scopeTest) }
|
||||
|
||||
/** True when the comment points at an external identifier (`I` tag), e.g. a hashtag, geohash or url. */
|
||||
fun hasRootScopeIdentifier() = tags.any { RootIdentifierTag.match(it) }
|
||||
|
||||
fun hasRootScopeKind(kind: String) = tags.any(RootKindTag::isKind, kind)
|
||||
|
||||
fun hasReplyScopeKind(kind: String) = tags.any(ReplyKindTag::isKind, kind)
|
||||
|
||||
Reference in New Issue
Block a user