015f300941651035945b5cdd98aeada97e4337aa / frogue / src / views / comments.rs
Raw
//! Comment views for tickets and patch proposals.
use chrono::{DateTime, FixedOffset};
use maud::{Markup, PreEscaped, Render, html};
use super::components::{AuthorName, Complaint, DeleteForm, TextArea};
use crate::comments::CommentAuthor;
/// A comment as displayed in the UI.
pub struct CommentView<'a> {
/// The base URL we can find this comment at. MUST contain the "/comments" part.
pub base_url: &'a str,
/// The comment's UUID for edit/delete forms.
pub uuid: String,
/// The author's information (local or remote).
pub author: CommentAuthor,
/// Raw/Markdown comment content
pub content: String,
/// The comment content rendered as HTML.
pub content_html: String,
/// When the comment was created.
pub created_at: DateTime<FixedOffset>,
/// When the comment was last edited.
pub updated_at: Option<DateTime<FixedOffset>>,
/// Whether the current viewer may change (edit/delete) this comment.
pub may_change: bool,
}
impl Render for CommentView<'_> {
fn render(&self) -> Markup {
// let edit_action = format!("{}/{}", base_action, comment.uuid);
// let delete_action = format!("{}/{}/delete", base_action, comment.uuid);
html! {
article class="comment" id=(format!("comment-{}", self.uuid)) {
header class="comment-header" {
(AuthorName(&self.author))
" "
span class="comment-created" {
(self.created_at.format("%Y-%m-%d %H:%M UTC").to_string())
}
@if let Some(updated_at) = &self.updated_at {
span class="comment-updated" {
em {
" (last updated at "
(updated_at.format("%Y-%m-%d %H:%M UTC").to_string())
")"
}
}
}
}
div class="comment-content" {
(PreEscaped(&self.content_html))
}
@if self.may_change {
details class="comment-actions" {
summary { "Actions" }
form method="POST" action=(format!("{}/{}", self.base_url, self.uuid)) class="inline-form" {
(TextArea {
label: Some("Edit comment"),
name: "content",
rows: 4,
value: Some(&self.content),
required: true,
})
button type="submit" { "Save" }
}
(DeleteForm {
action: format!("{}/{}/delete", self.base_url, self.uuid),
form_class: Some("inline-form"),
button_class: Some("danger"),
confirm: Some("Delete this comment?"),
})
}
}
}
}
}
}
pub struct CommentListView<'a> {
pub comments: &'a [CommentView<'a>],
pub may_comment: bool,
pub base_comments_url: &'a str,
pub comment_error: Option<&'a str>,
pub comment_draft: Option<&'a str>,
}
impl Render for CommentListView<'_> {
fn render(&self) -> Markup {
html! {
section class="comments" {
h3 { "Comments" }
@if self.comments.is_empty() {
p class="no-comments" { "No comments yet." }
} @else {
@for c in self.comments {
(CommentView {
base_url: self.base_comments_url,
// TODO: find a solution that doesn't clone here
content_html: c.content_html.clone(),
author: c.author.clone(),
uuid: c.uuid.clone(),
content: c.content.clone(),
..*c
})
}
}
@if self.may_comment {
(Complaint(self.comment_error))
form method="POST" action=(self.base_comments_url) class="comment-form" {
(TextArea {
label: Some("Add a comment (Markdown supported)"),
name: "content",
rows: 4,
value: self.comment_draft,
required: true,
})
button type="submit" { "Comment" }
}
}
}
}
}
}