text/x-rust
•
3.60 KB
•
140 lines
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use super::common::format_relative_time;
/// Tag changes parsed from a comment
#[derive(Debug, Clone, Default)]
pub struct TagChanges {
pub added: Vec<String>,
pub removed: Vec<String>,
}
impl TagChanges {
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty()
}
}
/// Status of an issue
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum IssueStatus {
#[default]
Open,
Completed,
Cancelled,
}
impl IssueStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Open => "open",
Self::Completed => "completed",
Self::Cancelled => "cancelled",
}
}
}
/// Frontmatter parsed from issue comment files
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommentFrontmatter {
#[serde(with = "time::serde::rfc3339")]
pub date: OffsetDateTime,
pub author: String,
pub email: String,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub status: Option<IssueStatus>,
#[serde(default)]
pub tags: Option<String>,
}
/// A single comment on an issue
#[derive(Debug, Clone)]
pub struct IssueComment {
pub date: OffsetDateTime,
pub author: String,
pub author_name: String,
pub content_html: String,
pub status_change: Option<IssueStatus>,
pub tag_changes: TagChanges,
}
/// An issue with all its comments
#[derive(Debug, Clone)]
pub struct Issue {
pub dir_name: String,
pub title: String,
pub status: IssueStatus,
pub tags: Vec<String>,
pub comments: Vec<IssueComment>,
}
/// Summary for list view (without loading all comments)
#[derive(Debug, Clone)]
pub struct IssueSummary {
pub dir_name: String,
pub title: String,
pub created_at: OffsetDateTime,
pub author: String,
pub author_name: String,
pub status: IssueStatus,
pub tags: Vec<String>,
pub comment_count: usize,
}
impl Issue {
/// Determine current status from comments (last status-changing comment wins)
pub fn compute_status(comments: &[IssueComment]) -> IssueStatus {
comments
.iter()
.rev()
.find_map(|c| c.status_change)
.unwrap_or_default()
}
/// Compute final tags by accumulating changes across all comments
pub fn compute_tags(comments: &[IssueComment]) -> Vec<String> {
let mut tags: Vec<String> = Vec::new();
for comment in comments {
// Add new tags
for tag in &comment.tag_changes.added {
if !tags.contains(tag) {
tags.push(tag.clone());
}
}
// Remove tags
for tag in &comment.tag_changes.removed {
tags.retain(|t| t != tag);
}
}
tags.sort();
tags
}
}
impl IssueSummary {
/// URI for viewing this issue
pub fn uri(&self, owner: &str, project_slug: &str) -> String {
format!("/~{}/{}/talk/{}", owner, project_slug, self.dir_name)
}
/// Format the created_at date for display
pub fn relative_time(&self) -> String {
let now = OffsetDateTime::now_utc();
let diff = now - self.created_at;
format_relative_time(diff.whole_seconds())
}
}
impl IssueComment {
/// Format the date for display
pub fn relative_time(&self) -> String {
let now = OffsetDateTime::now_utc();
let diff = now - self.date;
format_relative_time(diff.whole_seconds())
}
}