Login
4 branches 0 tags
Ben (Desktop/Arch) Code cleanup ada8ea6 11 days ago 251 Commits
rubhub / src / models / issue.rs
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())
    }
}