Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 52 additions & 43 deletions src/ui/components/issue_conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ use rat_cursor::HasScreenCursor;
use rat_widget::{
event::{HandleEvent, Outcome, TextOutcome, ct_event},
focus::{FocusBuilder, FocusFlag, HasFocus, Navigation},
line_number::{LineNumberState, LineNumbers},
list::{ListState, selection::RowSelection},
paragraph::{Paragraph, ParagraphState},
textarea::{TextArea, TextAreaState, TextWrap},
};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style, Stylize},
layout::{Rect, Spacing},
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{self, Block, ListItem, StatefulWidget, Widget},
widgets::{self, Block, Borders, ListItem, Padding, StatefulWidget, Widget},
};
use ratatui_macros::{horizontal, line, span, vertical};
use ratatui_macros::{horizontal, line, vertical};
use std::{
collections::{HashMap, HashSet},
sync::{Arc, OnceLock, RwLock},
Expand Down Expand Up @@ -170,7 +171,7 @@ pub struct TimelineEventView {
}

impl TimelineEventView {
fn from_api(event: TimelineEvent, fallback_id: u64) -> Option<Self> {
pub(crate) fn from_api(event: TimelineEvent, fallback_id: u64) -> Option<Self> {
if matches!(
event.event,
IssueEvent::Commented | IssueEvent::LineCommented | IssueEvent::CommentDeleted
Expand Down Expand Up @@ -210,6 +211,7 @@ impl TimelineEventView {

pub struct IssueConversation {
title: Option<Arc<str>>,
ln_state: LineNumberState,
action_tx: Option<tokio::sync::mpsc::Sender<Action>>,
current: Option<IssueConversationSeed>,
cache_number: Option<u64>,
Expand Down Expand Up @@ -264,13 +266,13 @@ enum MessageKey {
}

#[derive(Debug, Clone, Default)]
struct MarkdownRender {
lines: Vec<Line<'static>>,
links: Vec<RenderedLink>,
pub(crate) struct MarkdownRender {
pub(crate) lines: Vec<Line<'static>>,
pub(crate) links: Vec<RenderedLink>,
}

#[derive(Debug, Clone)]
struct RenderedLink {
pub(crate) struct RenderedLink {
line: usize,
col: usize,
label: String,
Expand Down Expand Up @@ -314,6 +316,7 @@ impl IssueConversation {
action_tx: None,
current: None,
cache_number: None,
ln_state: LineNumberState::default(),
cache_comments: Vec::new(),
timeline_cache_number: None,
cache_timeline: Vec::new(),
Expand Down Expand Up @@ -358,48 +361,40 @@ impl IssueConversation {
return;
}
self.area = area.main_content;
let title = self.title.clone().unwrap_or_default();
let wrapped_title = wrap(&title, area.main_content.width.saturating_sub(2) as usize);
let title_para_height = wrapped_title.len() as u16 + 2;
let last_item = wrapped_title.last();
let last_line = last_item
.as_ref()
.map(|l| {
line![
l.to_string(),
span!(
" #{}",
self.current.as_ref().map(|s| s.number).unwrap_or_default()
)
.dim()
]
})
.unwrap_or_else(|| Line::from(""));
let wrapped_title_len = wrapped_title.len() as u16;
let title_para = Text::from_iter(
wrapped_title
.into_iter()
.take(wrapped_title_len as usize - 1)
.map(Line::from)
.chain(std::iter::once(last_line)),
);
let mut title = self.title.clone().unwrap_or_default().to_string();
title.push_str(&format!(
" #{}",
self.current.as_ref().map(|s| s.number).unwrap_or_default()
));
let title = title.trim();
let wrapped_title = wrap(title, area.main_content.width.saturating_sub(2) as usize);
let title_para_height = wrapped_title.len() as u16 + 1;
let title_para = Text::from_iter(wrapped_title);

let areas = vertical![==title_para_height, *=1, ==5].split(area.main_content);
let title_area = areas[0];
let content_area = areas[1];
let input_area = areas[2];
let content_split = horizontal![*=1, *=1].split(content_area);
let content_split = horizontal![*=1, *=1]
.spacing(Spacing::Overlap(1))
.split(content_area);
let list_area = content_split[0];
let body_area = content_split[1];
let items = self.build_items(list_area, body_area);

let title_widget = widgets::Paragraph::new(title_para)
.block(Block::bordered().border_type(ratatui::widgets::BorderType::Rounded))
.block(
Block::default()
.padding(Padding::horizontal(1))
.borders(Borders::BOTTOM)
.merge_borders(ratatui::symbols::merge::MergeStrategy::Exact),
)
.style(Style::default().add_modifier(Modifier::BOLD));
title_widget.render(title_area, buf);

let mut list_block = Block::bordered()
.border_type(ratatui::widgets::BorderType::Rounded)
let mut list_block = Block::default()
.borders(Borders::RIGHT)
.merge_borders(ratatui::symbols::merge::MergeStrategy::Exact)
.border_style(get_border_style(&self.list_state));

if !self.is_loading_current() {
Expand Down Expand Up @@ -449,13 +444,26 @@ impl IssueConversation {

match self.textbox_state {
InputState::Input => {
let [line_numbers, input_area] = horizontal![==self.input_state.len_lines().checked_ilog10().unwrap_or(0) as u16 + 2, *=1].areas(input_area);
let ln_block = Block::default()
.borders(Borders::TOP)
.merge_borders(ratatui::symbols::merge::MergeStrategy::Exact)
.border_style(get_border_style(&self.input_state));
let ln = LineNumbers::new()
.with_textarea(&self.input_state)
.block(ln_block)
.style(Style::default().dim());
ln.render(line_numbers, buf, &mut self.ln_state);

let input_title = if let Some(err) = &self.post_error {
format!("Comment (Ctrl+Enter to send) | {err}")
} else {
"Comment (Ctrl+Enter to send)".to_string()
};
let mut input_block = Block::bordered()
.border_type(ratatui::widgets::BorderType::Rounded)
let mut input_block = Block::default()
.borders(Borders::TOP)
.merge_borders(ratatui::symbols::merge::MergeStrategy::Exact)
.padding(Padding::horizontal(1))
.border_style(get_border_style(&self.input_state));
if !self.posting {
input_block = input_block.title(input_title);
Expand Down Expand Up @@ -636,8 +644,9 @@ impl IssueConversation {

let body = Paragraph::new(body_lines)
.block(
Block::bordered()
.border_type(ratatui::widgets::BorderType::Rounded)
Block::default()
.borders(Borders::LEFT)
.merge_borders(ratatui::symbols::merge::MergeStrategy::Exact)
.border_style(get_border_style(&self.body_paragraph_state))
.title(if self.screen == MainScreen::DetailsFullscreen {
"Message Body (PageUp/PageDown/Home/End | f/Esc: exit fullscreen)"
Expand Down Expand Up @@ -2441,7 +2450,7 @@ pub(crate) fn render_markdown_lines(text: &str, width: usize, indent: usize) ->
render_markdown(text, width, indent).lines
}

fn render_markdown(text: &str, width: usize, indent: usize) -> MarkdownRender {
pub(crate) fn render_markdown(text: &str, width: usize, indent: usize) -> MarkdownRender {
let mut renderer = MarkdownRenderer::new(width, indent);
let options = Options::ENABLE_GFM
| Options::ENABLE_STRIKETHROUGH
Expand Down
152 changes: 152 additions & 0 deletions src/ui/components/issue_convo_preview.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use async_trait::async_trait;
use rat_widget::{
event::{HandleEvent, Regular},
focus::{FocusBuilder, FocusFlag, HasFocus},
paragraph::ParagraphState,
};
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{Block, Borders, StatefulWidget, Widget},
};
use std::sync::Arc;
use textwrap::wrap;

use crate::{
errors::AppError,
ui::{
Action,
components::{Component, help::HelpElementKind, issue_conversation::render_markdown},
layout::Layout,
utils::get_border_style,
},
};

pub const HELP: &[HelpElementKind] = &[
crate::help_text!("Issue Conversation Help"),
crate::help_keybind!("Up/Down", "select issue body/comment entry"),
crate::help_keybind!("PageUp/PageDown/Home/End", "scroll message body pane"),
crate::help_keybind!("t", "toggle timeline events"),
crate::help_keybind!("f", "toggle fullscreen body view"),
crate::help_keybind!("C", "close selected issue"),
crate::help_keybind!("l", "copy link to selected message"),
crate::help_keybind!("Enter (popup)", "confirm close reason"),
crate::help_keybind!("Ctrl+P", "toggle comment input/preview"),
crate::help_keybind!("e", "edit selected comment in external editor"),
crate::help_keybind!("r", "add reaction to selected comment"),
crate::help_keybind!("R", "remove reaction from selected comment"),
crate::help_keybind!("Ctrl+Enter / Alt+Enter", "send comment"),
crate::help_keybind!("Esc", "exit fullscreen / return to issue list"),
];

#[derive(Default)]
pub struct IssueConvoPreview {
action_tx: Option<tokio::sync::mpsc::Sender<Action>>,
body: Option<Arc<str>>,
area: Rect,
paragraph_state: ParagraphState,
index: usize,
focus: FocusFlag,
}

impl IssueConvoPreview {
pub fn new() -> Self {
Self::default()
}

pub fn render(&mut self, area: Layout, buf: &mut Buffer) {
let block_template = Block::default()
.borders(Borders::LEFT | Borders::BOTTOM)
.border_style(get_border_style(&self.paragraph_state));

self.area = area.mini_convo_preview;
let Some(ref body) = self.body else {
let para =
ratatui::widgets::Paragraph::new("Select an issue to preview the conversation")
.block(
block_template
.title(format!("[{}] Issue Conversation]", self.index))
.merge_borders(ratatui::symbols::merge::MergeStrategy::Exact),
);
para.render(area.mini_convo_preview, buf);
return;
};
let body_str = wrap(
body,
area.mini_convo_preview.width.saturating_sub(2) as usize,
)
.join("\n");
let rendered = render_markdown(
&body_str,
area.mini_convo_preview.width.saturating_sub(2).into(),
2,
)
.lines;
let para = rat_widget::paragraph::Paragraph::new(rendered).block(
Block::default()
.borders(Borders::LEFT | Borders::BOTTOM)
.title(format!("[{}] Issue Body", self.index))
.merge_borders(ratatui::symbols::merge::MergeStrategy::Exact)
.border_style(get_border_style(&self.paragraph_state)),
);
para.render(area.mini_convo_preview, buf, &mut self.paragraph_state);
}
}

#[async_trait(?Send)]
impl Component for IssueConvoPreview {
fn render(&mut self, area: Layout, buf: &mut Buffer) {
self.render(area, buf);
}

fn register_action_tx(&mut self, action_tx: tokio::sync::mpsc::Sender<Action>) {
self.action_tx = Some(action_tx);
}

async fn handle_event(&mut self, event: Action) -> Result<(), AppError> {
match event {
Action::AppEvent(ref event) => {
self.paragraph_state.handle(event, Regular);
}
Action::ChangeIssueBodyPreview(body) => {
self.body = Some(body);
}
_ => {}
}
Ok(())
}

fn should_render(&self) -> bool {
true
}

fn is_animating(&self) -> bool {
false
}

fn set_index(&mut self, index: usize) {
self.index = index;
}

fn set_global_help(&self) {
if let Some(action_tx) = &self.action_tx {
let _ = action_tx.try_send(Action::SetHelp(HELP));
}
}
}

impl HasFocus for IssueConvoPreview {
fn build(&self, builder: &mut FocusBuilder) {
let tag = builder.start(self);
builder.widget(&self.paragraph_state);
builder.end(tag);
}

fn focus(&self) -> FocusFlag {
self.focus.clone()
}

fn area(&self) -> Rect {
self.area
}
}
19 changes: 6 additions & 13 deletions src/ui/components/issue_detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use ratatui::{
layout::{Constraint, Direction, Layout as RtLayout, Rect},
prelude::Widget,
style::Style,
symbols::merge::MergeStrategy,
text::{Line, Span, Text},
widgets::{Block, Paragraph, Wrap},
widgets::{Block, Borders, Paragraph, Wrap},
};
use ratatui_macros::line;

Expand Down Expand Up @@ -115,9 +116,10 @@ impl IssuePreview {

pub fn render(&mut self, area: Layout, buf: &mut Buffer) {
self.area = area.issue_preview;
let block = Block::bordered()
.border_type(ratatui::widgets::BorderType::Rounded)
.title("Issue Info");
let block = Block::default()
.borders(Borders::LEFT | Borders::TOP)
.border_style(Style::new().dim())
.merge_borders(MergeStrategy::Exact);

let inner = block.inner(area.issue_preview);
block.render(area.issue_preview, buf);
Expand Down Expand Up @@ -169,15 +171,6 @@ impl IssuePreview {
_ => Style::new().cyan(),
};

let kind = if seed.is_pull_request {
"Pull Request"
} else {
"Issue"
};
lines.push(Line::from(vec![
Span::styled("Type: ", label_style),
Span::styled(kind, Style::new().cyan()),
]));
lines.push(Line::from(vec![
Span::styled("State: ", label_style),
Span::styled(format!("{:?}", seed.state), state_style),
Expand Down
Loading
Loading