From: Lanius Trolling Date: Tue, 14 Feb 2023 15:40:12 +0000 (-0500) Subject: Add reply links to original comments X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=b65ea8a848d9b61d82de7a25f239758ee8a1e0c4;p=factbooks Add reply links to original comments --- diff --git a/src/main/kotlin/info/mechyrdia/data/comments.kt b/src/main/kotlin/info/mechyrdia/data/comments.kt index c29ba71..38397c9 100644 --- a/src/main/kotlin/info/mechyrdia/data/comments.kt +++ b/src/main/kotlin/info/mechyrdia/data/comments.kt @@ -1,6 +1,8 @@ package info.mechyrdia.data import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toSet import kotlinx.serialization.Contextual import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -39,3 +41,40 @@ data class Comment( } } } + +@Serializable +data class CommentReplyLink( + @SerialName("_id") + override val id: Id = Id(), + + val originalPost: Id, + val replyingPost: Id +) : DataDocument { + companion object : TableHolder { + override val Table: DocumentTable = DocumentTable() + + override suspend fun initialize() { + Table.index(CommentReplyLink::originalPost) + Table.index(CommentReplyLink::replyingPost) + Table.unique(CommentReplyLink::originalPost, CommentReplyLink::replyingPost) + } + + suspend fun updateComment(updatedReply: Id, repliesTo: Set>) { + Table.remove(CommentReplyLink::replyingPost eq updatedReply) + Table.put( + repliesTo.map { original -> + CommentReplyLink( + originalPost = original, + replyingPost = updatedReply + ) + } + ) + } + + suspend fun deleteComment(deletedReply: Id) = updateComment(deletedReply, emptySet()) + + suspend fun getReplies(original: Id): Set> { + return Table.filter(CommentReplyLink::originalPost eq original).map { it.replyingPost }.toSet() + } + } +} diff --git a/src/main/kotlin/info/mechyrdia/data/view_comments.kt b/src/main/kotlin/info/mechyrdia/data/view_comments.kt index 320eb0f..17402c1 100644 --- a/src/main/kotlin/info/mechyrdia/data/view_comments.kt +++ b/src/main/kotlin/info/mechyrdia/data/view_comments.kt @@ -7,6 +7,8 @@ import info.mechyrdia.lore.TextParserCommentTags import info.mechyrdia.lore.TextParserState import info.mechyrdia.lore.dateTime import io.ktor.server.application.* +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.html.* import java.time.Instant @@ -21,26 +23,33 @@ data class CommentRenderData( val lastEdit: Instant?, val contentsRaw: String, - val contentsHtml: String + val contentsHtml: String, + + val replyLinks: Set>, ) { companion object { suspend operator fun invoke(comments: List, nations: MutableMap, NationData> = mutableMapOf()): List { - return comments.mapNotNull { comment -> - val nationData = nations.getNation(comment.submittedBy) - val htmlResult = TextParserState.parseText(comment.contents, TextParserCommentTags.asTags, Unit) - - if (htmlResult.succeeded) - CommentRenderData( - comment.id, - nationData, - comment.submittedIn, - comment.submittedAt, - comment.numEdits, - comment.lastEdit, - comment.contents, - htmlResult.html - ) - else null + return coroutineScope { + comments.mapNotNull { comment -> + val nationData = nations.getNation(comment.submittedBy) + val htmlResult = TextParserState.parseText(comment.contents, TextParserCommentTags.asTags, Unit) + + if (htmlResult.succeeded) + async { + CommentRenderData( + id = comment.id, + submittedBy = nationData, + submittedIn = comment.submittedIn, + submittedAt = comment.submittedAt, + numEdits = comment.numEdits, + lastEdit = comment.lastEdit, + contentsRaw = comment.contents, + contentsHtml = htmlResult.html, + replyLinks = CommentReplyLink.getReplies(comment.id), + ) + } + else null + }.map { it.await() } } } } @@ -98,6 +107,14 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id>$reply" + } + } + } } } diff --git a/src/main/kotlin/info/mechyrdia/data/views_comment.kt b/src/main/kotlin/info/mechyrdia/data/views_comment.kt index 573b4ea..75edeff 100644 --- a/src/main/kotlin/info/mechyrdia/data/views_comment.kt +++ b/src/main/kotlin/info/mechyrdia/data/views_comment.kt @@ -45,7 +45,7 @@ suspend fun ApplicationCall.recentCommentsPage(): HTML.() -> Unit { if (limit == validLimit) +"$validLimit" else - a(href="/comment/recent?limit=$validLimit") { + a(href = "/comment/recent?limit=$validLimit") { +"$validLimit" } } @@ -79,6 +79,7 @@ suspend fun ApplicationCall.newCommentRoute(): Nothing { ) Comment.Table.put(comment) + CommentReplyLink.updateComment(comment.id, getReplies(contents)) redirect("/lore/$pagePath#comment-${comment.id}") } @@ -111,6 +112,7 @@ suspend fun ApplicationCall.editCommentRoute(): Nothing { ) Comment.Table.put(newComment) + CommentReplyLink.updateComment(commentId, getReplies(newContents)) redirect("/comment/view/$commentId") } @@ -156,6 +158,7 @@ suspend fun ApplicationCall.deleteCommentRoute(): Nothing { throw ForbiddenException("Illegal attempt by ${currNation.id} to delete comment by ${comment.submittedBy}") Comment.Table.del(commentId) + CommentReplyLink.deleteComment(commentId) redirect("/lore/${comment.submittedIn}#comments") } @@ -430,6 +433,15 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin td { +"[reply](comment id)[/reply]" } td { +"Creates a reply link to a comment" } } + tr { + td { +"[quote]Quoted text[/quote]" } + td { + +"Creates a " + blockQuote { + +"block-level quote" + } + } + } tr { td { +"[lang=tylan]Rheagda Tulasra[/lang]" } td { diff --git a/src/main/kotlin/info/mechyrdia/lore/parser_reply.kt b/src/main/kotlin/info/mechyrdia/lore/parser_reply.kt new file mode 100644 index 0000000..1e59569 --- /dev/null +++ b/src/main/kotlin/info/mechyrdia/lore/parser_reply.kt @@ -0,0 +1,37 @@ +package info.mechyrdia.lore + +import info.mechyrdia.data.Comment +import info.mechyrdia.data.Id + +class CommentRepliesBuilder { + private val repliesTo = mutableSetOf>() + + fun addReplyTag(reply: Id) { + repliesTo += reply + } + + fun toReplySet() = repliesTo.toSet() +} + +enum class TextParserReplyCounterTag(val type: TextParserTagType) { + REPLY( + TextParserTagType.Indirect { _, content, builder -> + builder.addReplyTag(Id(sanitizeLink(content))) + "[reply]$content[/reply]" + } + ); + + companion object { + val asTags: TextParserTags + get() = TextParserTags(values().associate { it.name to it.type }) + } +} + +fun getReplies(commentContents: String): Set> { + val builder = CommentRepliesBuilder() + + if (!TextParserState.parseText(commentContents, TextParserReplyCounterTag.asTags, builder).succeeded) + return emptySet() + + return builder.toReplySet() +} diff --git a/src/main/kotlin/info/mechyrdia/lore/parser_tags.kt b/src/main/kotlin/info/mechyrdia/lore/parser_tags.kt index a83e8e9..3fbfdf5 100644 --- a/src/main/kotlin/info/mechyrdia/lore/parser_tags.kt +++ b/src/main/kotlin/info/mechyrdia/lore/parser_tags.kt @@ -314,6 +314,8 @@ enum class TextParserCommentTags(val type: TextParserTagType) { TH(TextParserFormattingTag.TH.type), URL(TextParserFormattingTag.EXTLINK.type), + LANG(TextParserFormattingTag.LANG.type), + REPLY( TextParserTagType.Indirect { _, content, _ -> val id = sanitizeLink(content) @@ -322,7 +324,13 @@ enum class TextParserCommentTags(val type: TextParserTagType) { } ), - LANG(TextParserFormattingTag.LANG.type) + QUOTE( + TextParserTagType.Direct({ _, _ -> + "
" + }, { _ -> + "
" + }) + ) ; companion object { @@ -331,50 +339,6 @@ enum class TextParserCommentTags(val type: TextParserTagType) { } } -enum class TextParserToCBuilderTag(val type: TextParserTagType) { - H1( - TextParserTagType.Indirect { _, content, builder -> - builder.add(headerContentToLabel(content), 0, headerContentToAnchor(content)) - "[h1]$content[/h1]" - } - ), - H2( - TextParserTagType.Indirect { _, content, builder -> - builder.add(headerContentToLabel(content), 1, headerContentToAnchor(content)) - "[h2]$content[/h2]" - } - ), - H3( - TextParserTagType.Indirect { _, content, builder -> - builder.add(headerContentToLabel(content), 2, headerContentToAnchor(content)) - "[h3]$content[/h3]" - } - ), - H4( - TextParserTagType.Indirect { _, content, builder -> - builder.add(headerContentToLabel(content), 3, headerContentToAnchor(content)) - "[h4]$content[/h4]" - } - ), - H5( - TextParserTagType.Indirect { _, content, builder -> - builder.add(headerContentToLabel(content), 4, headerContentToAnchor(content)) - "[h5]$content[/h5]" - } - ), - H6( - TextParserTagType.Indirect { _, content, builder -> - builder.add(headerContentToLabel(content), 5, headerContentToAnchor(content)) - "[h6]$content[/h6]" - } - ); - - companion object { - val asTags: TextParserTags - get() = TextParserTags(values().associate { it.name to it.type }) - } -} - val NON_LINK_CHAR = Regex("[^#a-zA-Z\\d\\-._]") val DOT_CHARS = Regex("\\.+") fun sanitizeLink(html: String) = html.replace(NON_LINK_CHAR, "").replace(DOT_CHARS, ".") diff --git a/src/main/kotlin/info/mechyrdia/lore/parser_toc.kt b/src/main/kotlin/info/mechyrdia/lore/parser_toc.kt index 119b34d..b14ba19 100644 --- a/src/main/kotlin/info/mechyrdia/lore/parser_toc.kt +++ b/src/main/kotlin/info/mechyrdia/lore/parser_toc.kt @@ -35,3 +35,47 @@ class TableOfContentsBuilder { fun toNavBar(): List = listOf(NavLink("#page-top", title!!, aClasses = "left")) + links.toList() } + +enum class TextParserToCBuilderTag(val type: TextParserTagType) { + H1( + TextParserTagType.Indirect { _, content, builder -> + builder.add(headerContentToLabel(content), 0, headerContentToAnchor(content)) + "[h1]$content[/h1]" + } + ), + H2( + TextParserTagType.Indirect { _, content, builder -> + builder.add(headerContentToLabel(content), 1, headerContentToAnchor(content)) + "[h2]$content[/h2]" + } + ), + H3( + TextParserTagType.Indirect { _, content, builder -> + builder.add(headerContentToLabel(content), 2, headerContentToAnchor(content)) + "[h3]$content[/h3]" + } + ), + H4( + TextParserTagType.Indirect { _, content, builder -> + builder.add(headerContentToLabel(content), 3, headerContentToAnchor(content)) + "[h4]$content[/h4]" + } + ), + H5( + TextParserTagType.Indirect { _, content, builder -> + builder.add(headerContentToLabel(content), 4, headerContentToAnchor(content)) + "[h5]$content[/h5]" + } + ), + H6( + TextParserTagType.Indirect { _, content, builder -> + builder.add(headerContentToLabel(content), 5, headerContentToAnchor(content)) + "[h6]$content[/h6]" + } + ); + + companion object { + val asTags: TextParserTags + get() = TextParserTags(values().associate { it.name to it.type }) + } +} diff --git a/src/main/kotlin/info/mechyrdia/lore/view_bar.kt b/src/main/kotlin/info/mechyrdia/lore/view_bar.kt index cb05709..aba1c2e 100644 --- a/src/main/kotlin/info/mechyrdia/lore/view_bar.kt +++ b/src/main/kotlin/info/mechyrdia/lore/view_bar.kt @@ -1,6 +1,8 @@ package info.mechyrdia.lore -import kotlinx.html.* +import kotlinx.html.ASIDE +import kotlinx.html.TagConsumer +import kotlinx.html.div abstract class Sidebar { protected abstract fun TagConsumer<*>.display() diff --git a/src/main/kotlin/info/mechyrdia/lore/view_nav.kt b/src/main/kotlin/info/mechyrdia/lore/view_nav.kt index 03e17f5..f0763d0 100644 --- a/src/main/kotlin/info/mechyrdia/lore/view_nav.kt +++ b/src/main/kotlin/info/mechyrdia/lore/view_nav.kt @@ -8,21 +8,9 @@ import kotlinx.html.DIV import kotlinx.html.a import kotlinx.html.span import kotlinx.html.style -import kotlin.collections.List -import kotlin.collections.Map import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.dropLast -import kotlin.collections.emptyMap -import kotlin.collections.iterator -import kotlin.collections.joinToString -import kotlin.collections.listOf -import kotlin.collections.mapIndexed -import kotlin.collections.mapOf -import kotlin.collections.orEmpty -import kotlin.collections.plus import kotlin.collections.set -import kotlin.collections.take suspend fun ApplicationCall.standardNavBar(path: List? = null) = listOf( NavLink("/lore", "Lore Index") diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css index f18d467..a2d9205 100644 --- a/src/main/resources/static/style.css +++ b/src/main/resources/static/style.css @@ -869,12 +869,13 @@ textarea.lang-tylan { .comment-box { border: 0.25em solid var(--comment-stroke); background-color: var(--comment-fill); - padding: 0.75em; + padding: 0.75em 0.5em 0.25em; margin: 1em 0; } .comment-box > .comment-author { display: flex; + align-items: center; } .comment-box > .comment-author > .flag-icon { @@ -892,14 +893,12 @@ textarea.lang-tylan { font-weight: bold; text-align: left; - vertical-align: center; flex-grow: 1; flex-shrink: 0; } .comment-box > .comment-author > .posted-at { text-align: right; - vertical-align: center; flex-grow: 1; flex-shrink: 1; } @@ -910,6 +909,13 @@ textarea.lang-tylan { padding-top: 0.5em; } +blockquote { + margin: 0.25em 0; + border-left: 0.25em solid var(--comment-stroke); + padding: 0.5em; + background-color: rgba(0, 0, 0, 0.134); +} + .comment-edit-box { display: none; }