diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
index 5b88fb9f4..3214a11b2 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
@@ -96,6 +96,7 @@ import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
+import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
@@ -395,6 +396,41 @@ object LocalCache {
}
}
+ fun consume(
+ event: WikiNoteEvent,
+ relay: Relay?,
+ ) {
+ val version = getOrCreateNote(event.id)
+ val note = getOrCreateAddressableNote(event.address())
+ val author = getOrCreateUser(event.pubKey)
+
+ if (version.event == null) {
+ version.loadEvent(event, author, emptyList())
+ version.moveAllReferencesTo(note)
+ }
+
+ if (relay != null) {
+ author.addRelayBeingUsed(relay, event.createdAt)
+ note.addRelay(relay)
+ }
+
+ // Already processed this event.
+ if (note.event?.id() == event.id()) return
+
+ if (antiSpam.isSpam(event, relay)) {
+ relay?.let { it.spamCounter++ }
+ return
+ }
+
+ val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
+
+ if (event.createdAt > (note.createdAt() ?: 0)) {
+ note.loadEvent(event, author, replyTo)
+
+ refreshObservers(note)
+ }
+ }
+
fun consume(
event: PollNoteEvent,
relay: Relay? = null,
@@ -1848,6 +1884,7 @@ object LocalCache {
is TextNoteEvent -> consume(event, relay)
is VideoHorizontalEvent -> consume(event, relay)
is VideoVerticalEvent -> consume(event, relay)
+ is WikiNoteEvent -> consume(event, relay)
else -> {
Log.w("Event Not Supported", event.toJson())
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt
index db4e2a79d..e25618332 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt
@@ -225,6 +225,7 @@ import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.VideoEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
+import com.vitorpamplona.quartz.events.WikiNoteEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -1160,6 +1161,9 @@ private fun RenderNoteRow(
is LongTextNoteEvent -> {
RenderLongFormContent(baseNote, accountViewModel, nav)
}
+ is WikiNoteEvent -> {
+ RenderWikiContent(baseNote, accountViewModel, nav)
+ }
is BadgeAwardEvent -> {
RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
}
@@ -2916,12 +2920,19 @@ private fun LoadAndDisplayUrl(url: String) {
}
@Composable
-private fun LoadAndDisplayUser(
+fun LoadAndDisplayUser(
userBase: User,
nav: (String) -> Unit,
) {
- val route = remember { "User/${userBase.pubkeyHex}" }
+ LoadAndDisplayUser(userBase, "User/${userBase.pubkeyHex}", nav)
+}
+@Composable
+fun LoadAndDisplayUser(
+ userBase: User,
+ route: String,
+ nav: (String) -> Unit,
+) {
val userState by userBase.live().metadata.observeAsState()
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userTags =
@@ -3660,6 +3671,92 @@ private fun LongFormHeader(
}
summary?.let {
+ Spacer(modifier = StdVertSpacer)
+ Text(
+ text = it,
+ style = MaterialTheme.typography.bodySmall,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
+ color = Color.Gray,
+ maxLines = 3,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun RenderWikiContent(
+ note: Note,
+ accountViewModel: AccountViewModel,
+ nav: (String) -> Unit,
+) {
+ val noteEvent = note.event as? WikiNoteEvent ?: return
+
+ WikiNoteHeader(noteEvent, note, accountViewModel, nav)
+}
+
+@Composable
+private fun WikiNoteHeader(
+ noteEvent: WikiNoteEvent,
+ note: Note,
+ accountViewModel: AccountViewModel,
+ nav: (String) -> Unit,
+) {
+ val title = remember(noteEvent) { noteEvent.title() }
+ val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() }
+ val summary =
+ remember(noteEvent) {
+ noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null }
+ }
+ val image = remember(noteEvent) { noteEvent.image() }
+
+ Row(
+ modifier =
+ Modifier
+ .padding(top = Size5dp)
+ .clip(shape = QuoteBorder)
+ .border(
+ 1.dp,
+ MaterialTheme.colorScheme.subtleBorder,
+ QuoteBorder,
+ ),
+ ) {
+ Column {
+ val automaticallyShowUrlPreview = remember { accountViewModel.settings.showUrlPreview.value }
+
+ if (automaticallyShowUrlPreview) {
+ image?.let {
+ AsyncImage(
+ model = it,
+ contentDescription =
+ stringResource(
+ R.string.preview_card_image_for,
+ it,
+ ),
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ ?: CreateImageHeader(note, accountViewModel)
+ }
+
+ title?.let {
+ Text(
+ text = it,
+ style = MaterialTheme.typography.bodyLarge,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(start = 10.dp, end = 10.dp, top = 10.dp),
+ )
+ }
+
+ summary?.let {
+ Spacer(modifier = StdVertSpacer)
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt
index 36b6f75fb..a2e7ff250 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt
@@ -76,21 +76,27 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
+import com.vitorpamplona.amethyst.model.AddressableNote
+import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
+import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
+import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingCommunityInPost
import com.vitorpamplona.amethyst.ui.elements.DisplayFollowingHashtagsInPost
import com.vitorpamplona.amethyst.ui.elements.DisplayPoW
import com.vitorpamplona.amethyst.ui.elements.DisplayReward
import com.vitorpamplona.amethyst.ui.elements.DisplayZapSplits
import com.vitorpamplona.amethyst.ui.elements.Reward
+import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.AudioHeader
import com.vitorpamplona.amethyst.ui.note.AudioTrackHeader
@@ -104,6 +110,8 @@ import com.vitorpamplona.amethyst.ui.note.DisplayRelaySet
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.HiddenNote
+import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
+import com.vitorpamplona.amethyst.ui.note.LoadAndDisplayUser
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu
@@ -131,6 +139,7 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
+import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.selectedNote
@@ -144,6 +153,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent
+import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
@@ -155,11 +165,13 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.RelaySetEvent
import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.VideoEvent
+import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@Composable
@@ -444,6 +456,8 @@ fun NoteMaster(
BadgeDisplay(baseNote = note)
} else if (noteEvent is LongTextNoteEvent) {
RenderLongFormHeaderForThread(noteEvent)
+ } else if (noteEvent is WikiNoteEvent) {
+ RenderWikiHeaderForThread(noteEvent, accountViewModel, nav)
} else if (noteEvent is ClassifiedsEvent) {
RenderClassifiedsReaderForThread(noteEvent, note, accountViewModel, nav)
}
@@ -797,3 +811,137 @@ private fun RenderLongFormHeaderForThread(noteEvent: LongTextNoteEvent) {
}
}
}
+
+@Preview
+@Composable
+private fun RenderWikiHeaderForThreadPreview() {
+ val event = Event.fromJson("{\"id\":\"277f982a4cd3f67cc47ad9282176acabee1713848f547d6021e0c155572078e1\",\"pubkey\":\"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c\",\"created_at\":1708695717,\"kind\":30818,\"tags\":[[\"d\",\"amethyst\"],[\"a\",\"30818:f03e7c5262648e0b7823dfb49f8f17309cfec9cb14711413dcabdf3d7fc6369a:amethyst\",\"wss://relay.nostr.band\",\"fork\"],[\"e\",\"ceabc60c8022c472c727aa25ae7691885964366386ce265c47e5a78be6cb00be\",\"wss://relay.nostr.band\",\"fork\"],[\"title\",\"Amethyst\"],[\"published_at\",\"1708707133\"]],\"content\":\"An Android-only app written in Kotlin with support for over 90 event kinds. \\n\\n\\n\",\"sig\":\"6748126a909a20dbdb67947a09d64e41d7140a79335a4ad675c6173d7dd5dbcab9c360dec617bd67bbbc20dfad416b15056eda2e20716cd6c425a84301a125a0\"}") as WikiNoteEvent
+ val accountViewModel = mockAccountViewModel()
+ val nav: (String) -> Unit = {}
+
+ runBlocking {
+ withContext(Dispatchers.IO) {
+ LocalCache.justConsume(event, null)
+ }
+ }
+
+ LoadNote(baseNoteHex = "277f982a4cd3f67cc47ad9282176acabee1713848f547d6021e0c155572078e1", accountViewModel = accountViewModel) { baseNote ->
+ ThemeComparisonColumn(
+ onDark = {
+ val bg = MaterialTheme.colorScheme.background
+ val backgroundColor =
+ remember {
+ mutableStateOf(bg)
+ }
+
+ Column {
+ RenderWikiHeaderForThread(noteEvent = event, accountViewModel = accountViewModel, nav)
+ RenderTextEvent(
+ baseNote!!,
+ false,
+ true,
+ backgroundColor,
+ accountViewModel,
+ nav,
+ )
+ }
+ },
+ onLight = {
+ val bg = MaterialTheme.colorScheme.background
+ val backgroundColor =
+ remember {
+ mutableStateOf(bg)
+ }
+
+ Column {
+ RenderWikiHeaderForThread(noteEvent = event, accountViewModel = accountViewModel, nav)
+ RenderTextEvent(
+ baseNote!!,
+ false,
+ true,
+ backgroundColor,
+ accountViewModel,
+ nav,
+ )
+ }
+ },
+ )
+ }
+}
+
+@Composable
+private fun RenderWikiHeaderForThread(
+ noteEvent: WikiNoteEvent,
+ accountViewModel: AccountViewModel,
+ nav: (String) -> Unit,
+) {
+ val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() }
+
+ Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
+ Column {
+ noteEvent.image()?.let {
+ AsyncImage(
+ model = it,
+ contentDescription =
+ stringResource(
+ R.string.preview_card_image_for,
+ it,
+ ),
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+
+ noteEvent.title()?.let {
+ Spacer(modifier = DoubleVertSpacer)
+ Text(
+ text = it,
+ fontSize = 28.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+
+ forkedAddress?.let {
+ LoadAddressableNote(aTag = it, accountViewModel = accountViewModel) { originalVersion ->
+ if (originalVersion != null) {
+ ShowForkInformation(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav)
+ }
+ }
+ }
+
+ noteEvent
+ .summary()
+ ?.ifBlank { null }
+ ?.let {
+ Spacer(modifier = DoubleVertSpacer)
+ Text(
+ text = it,
+ modifier = Modifier.fillMaxWidth(),
+ color = Color.Gray,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ShowForkInformation(
+ originalVersion: AddressableNote,
+ modifier: Modifier = Modifier,
+ accountViewModel: AccountViewModel,
+ nav: (String) -> Unit,
+) {
+ val noteState by originalVersion.live().metadata.observeAsState()
+ val note = noteState?.note ?: return
+ val author = note.author ?: return
+ val route = remember(note) { routeFor(note, accountViewModel.userProfile()) }
+
+ if (route != null) {
+ Row(modifier) {
+ Text(stringResource(id = R.string.forked_from))
+ Spacer(modifier = StdHorzSpacer)
+ LoadAndDisplayUser(author, route, nav)
+ }
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5b0e87fa9..c48dd418b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -776,4 +776,5 @@
Thank you!
Max Limit
Restricted Writes
+ Forked from
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt
index ef6f61a20..39ba084ed 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BaseTextNoteEvent.kt
@@ -43,7 +43,7 @@ open class BaseTextNoteEvent(
fun mentions() = taggedUsers()
open fun replyTos(): List {
- val oldStylePositional = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
+ val oldStylePositional = tags.filter { it.size > 1 && it.size <= 3 && it[0] == "e" }.map { it[1] }
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1)
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1)
@@ -153,7 +153,10 @@ open class BaseTextNoteEvent(
fun tagsWithoutCitations(): List {
val repliesTo = replyTos()
val tagAddresses =
- taggedAddresses().filter { it.kind != CommunityDefinitionEvent.KIND }.map { it.toTag() }
+ taggedAddresses().filter {
+ it.kind != CommunityDefinitionEvent.KIND &&
+ it.kind != WikiNoteEvent.KIND
+ }.map { it.toTag() }
if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
val citations = findCitations()
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt
index b2fd06e2e..0c7956752 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt
@@ -123,6 +123,7 @@ class EventFactory {
VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig)
VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig)
VideoViewEvent.KIND -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig)
+ WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig)
else -> Event(id, pubKey, createdAt, kind, tags, content, sig)
}
}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt
new file mode 100644
index 000000000..25bb51454
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/WikiNoteEvent.kt
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) 2024 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.quartz.events
+
+import androidx.compose.runtime.Immutable
+import com.vitorpamplona.quartz.encoders.ATag
+import com.vitorpamplona.quartz.encoders.HexKey
+import com.vitorpamplona.quartz.signers.NostrSigner
+import com.vitorpamplona.quartz.utils.TimeUtils
+
+@Immutable
+class WikiNoteEvent(
+ id: HexKey,
+ pubKey: HexKey,
+ createdAt: Long,
+ tags: Array>,
+ content: String,
+ sig: HexKey,
+) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), AddressableEvent {
+ override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
+
+ override fun address() = ATag(kind, pubKey, dTag(), null)
+
+ fun topics() = hashtags()
+
+ fun forkFromAddress() =
+ tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let {
+ val aTagValue = it[1]
+ val relay = it.getOrNull(2)
+
+ ATag.parse(aTagValue, relay)
+ }
+
+ fun forkFromVersion() = tags.firstOrNull { it.size > 3 && it[0] == "e" && it[3] == "fork" }?.get(1)
+
+ fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
+
+ fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
+
+ fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
+
+ fun publishedAt() =
+ try {
+ tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull()
+ } catch (_: Exception) {
+ null
+ }
+
+ companion object {
+ const val KIND = 30818
+
+ fun create(
+ msg: String,
+ title: String?,
+ replyTos: List?,
+ mentions: List?,
+ signer: NostrSigner,
+ createdAt: Long = TimeUtils.now(),
+ onReady: (WikiNoteEvent) -> Unit,
+ ) {
+ val tags = mutableListOf>()
+ replyTos?.forEach { tags.add(arrayOf("e", it)) }
+ mentions?.forEach { tags.add(arrayOf("p", it)) }
+ title?.let { tags.add(arrayOf("title", it)) }
+ tags.add(arrayOf("alt", "Wiki Post: $title"))
+ signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
+ }
+ }
+}