mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-10-25 06:10:43 +08:00
253 lines
7.0 KiB
Rust
253 lines
7.0 KiB
Rust
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]
|
|
}
|
|
}
|