pub mod issue;
pub mod notification;
pub mod patch;
use std::collections::HashMap;
use std::fmt::Debug;
use radicle::cob::thread::{Comment, CommentId};
use radicle::cob::{ObjectId, Timestamp};
use radicle::identity::Did;
use radicle::issue::{Issue, IssueId};
use radicle::node::{Alias, AliasStore, NodeId};
use radicle::Profile;
use ratatui::prelude::*;
use ratatui::style::Stylize;
use tui_tree_widget::TreeItem;
use radicle_tui as tui;
use tui::ui::span;
use tui::ui::ToTree;
use super::format;
pub mod filter {
use std::fmt::{self, Write};
use std::str::FromStr;
use nom::bytes::complete::{tag_no_case, take};
use nom::character::complete::{char, multispace0};
use nom::combinator::map;
use nom::multi::separated_list1;
use nom::sequence::delimited;
use nom::{IResult, Parser};
use radicle::prelude::Did;
pub const FUZZY_MIN_SCORE: i64 = 50;
/// A generic filter that needs be implemented for item filters in order to
/// apply it.
pub trait Filter<T> {
fn matches(&self, item: &T) -> bool;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DidFilter {
Single(Did),
Or(Vec<Did>),
}
impl fmt::Display for DidFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DidFilter::Single(did) => write!(f, "{did}")?,
DidFilter::Or(dids) => {
let mut it = dids.iter().peekable();
f.write_char('(')?;
while let Some(did) = it.next() {
write!(f, "{did}")?;
if it.peek().is_none() {
write!(f, " or ")?;
}
}
f.write_char(')')?;
}
}
Ok(())
}
}
pub fn parse_did_single(input: &str) -> IResult<&str, DidFilter> {
let (input, did) = take(56_usize)(input)?;
match Did::from_str(did) {
Ok(did) => IResult::Ok((input, DidFilter::Single(did))),
Err(_) => IResult::Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
))),
}
}
pub fn parse_did_or(input: &str) -> IResult<&str, DidFilter> {
map(
delimited(
(multispace0, char('('), multispace0),
separated_list1(
delimited(multispace0, tag_no_case("or"), multispace0),
take(56_usize),
),
(multispace0, char(')'), multispace0),
),
|dids: Vec<&str>| {
DidFilter::Or(
dids.iter()
.filter_map(|did| Did::from_str(did).ok())
.collect::<Vec<_>>(),
)
},
)
.parse(input)
}
}
pub trait HasId {
fn id(&self) -> ObjectId;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthorItem {
pub nid: Option<NodeId>,
pub human_nid: Option<String>,
pub alias: Option<Alias>,
pub you: bool,
}
impl AuthorItem {
pub fn new(nid: Option<NodeId>, profile: &Profile) -> Self {
let alias = match nid {
Some(nid) => profile.alias(&nid),
None => None,
};
let you = nid.map(|nid| nid == *profile.id()).unwrap_or_default();
let human_nid = nid.map(|nid| format::did(&Did::from(nid)));
Self {
nid,
human_nid,
alias,
you,
}
}
}
/// A `CommentItem` represents a comment COB and is constructed from an `Issue` and
/// a `Comment`.
#[derive(Clone, Debug)]
pub struct CommentItem {
/// Comment OID.
pub id: CommentId,
/// Author of this comment.
pub author: AuthorItem,
/// The content of this comment.
pub body: String,
/// Reactions to this comment.
pub reactions: Vec<char>,
/// Time when patch was opened.
pub timestamp: Timestamp,
/// The parent OID if this is a reply.
pub reply_to: Option<CommentId>,
/// Replies to this comment.
pub replies: Vec<CommentItem>,
}
impl CommentItem {
pub fn new(profile: &Profile, issue: (IssueId, Issue), comment: (CommentId, Comment)) -> Self {
let (issue_id, issue) = issue;
let (comment_id, comment) = comment;
Self {
id: comment_id,
author: AuthorItem::new(Some(NodeId::from(*comment.author().0)), profile),
body: comment.body().to_string(),
reactions: comment.reactions().iter().map(|r| r.0.emoji()).collect(),
timestamp: comment.timestamp(),
reply_to: comment.reply_to(),
replies: issue
.thread()
.replies(&comment_id)
.map(|(reply_id, reply)| {
CommentItem::new(
profile,
(issue_id, issue.clone()),
(*reply_id, reply.clone()),
)
})
.collect(),
}
}
pub fn accumulated_reactions(&self) -> Vec<(char, usize)> {
let mut accumulated: HashMap<char, usize> = HashMap::new();
for reaction in &self.reactions {
if let Some(count) = accumulated.get_mut(reaction) {
*count = count.saturating_add(1);
} else {
accumulated.insert(*reaction, 1_usize);
}
}
let mut sorted = accumulated.into_iter().collect::<Vec<_>>();
sorted.sort();
sorted
}
pub fn path_to(&self, target_id: &CommentId, path: &mut Vec<CommentId>) -> bool {
path.push(self.id);
if self.id == *target_id {
return true;
}
for reply in &self.replies {
if reply.path_to(target_id, path) {
return true;
}
}
path.pop();
false
}
}
impl ToTree<String> for CommentItem {
fn rows(&self) -> Vec<TreeItem<'_, String>> {
let children = self.replies.iter().flat_map(CommentItem::rows).collect();
let author = match &self.author.alias {
Some(alias) => {
if self.author.you {
span::alias(&format!("{alias} (you)"))
} else {
span::alias(alias)
}
}
None => match &self.author.human_nid {
Some(nid) => span::alias(nid).dim(),
None => span::blank(),
},
};
let action = if self.reply_to.is_none() {
"opened"
} else {
"commented"
};
let timestamp = span::timestamp(&format::timestamp(&self.timestamp));
let text = Text::from(Line::from(
[author, " ".into(), action.into(), " ".into(), timestamp].to_vec(),
));
let item = TreeItem::new(self.id.to_string(), text, children)
.expect("Identifiers need to be unique");
vec![item]
}
}