diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt
index 8e72db0c1..86565a8f5 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt
@@ -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,
+ )
}
}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt
index 9c15c1513..ce5eb75ba 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt
@@ -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)
+ }
}
}
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index e9fd03b1c..3f6fbf18c 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -1475,6 +1475,9 @@
Hashtag-exclusive Post
Only followers of the hashtag will see it. Your general followers won\'t see it.
+ Comment on a website
+ Comment on an external resource
+
%1$d min read
diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt
index 1a2790d08..9566969ca 100644
--- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt
+++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt
@@ -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 }
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt
index 17b51a2ab..8bd085449 100644
--- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt
@@ -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)