Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
inbox: Show notifications w/ flux app
Erik Kundt committed 2 years ago
commit 9440519f9a1407bce8657d893b71332dd7fd1e6b
parent 5e18f3f0fd7060d17237336bdfeb0300f338d19b
4 files changed +198 -44
modified bin/commands/inbox/select/flux.rs
@@ -1,17 +1,19 @@
#[path = "flux/ui.rs"]
mod ui;

-
use std::default;
-

use anyhow::Result;

-
use radicle::node::notifications::Notification;
+
use radicle::node::notifications::NotificationId;
+

use radicle_tui as tui;
-
use tui::cob::inbox::Filter;
+

+
use tui::cob::inbox::{self, Filter};
use tui::context::Context;
use tui::flux::store::{State, Store};
use tui::flux::termination::{self, Interrupted};
+
use tui::flux::ui::cob::NotificationItem;
use tui::flux::ui::Frontend;
+
use tui::Exit;

use crate::tui_inbox::select::flux::ui::ListPage;

@@ -24,46 +26,69 @@ pub enum Mode {

pub struct App {
    context: Context,
-
    filter: Filter,
+
    _filter: Filter,
}

-
#[derive(Clone)]
+
#[derive(Clone, Debug)]
pub struct InboxState {
-
    notifications: Vec<String>,
+
    notifications: Vec<NotificationItem>,
+
    selected: Option<NotificationId>,
}

-
impl InboxState {
-
    pub fn notifications(&self) -> &Vec<String> {
-
        &self.notifications
+
impl TryFrom<&Context> for InboxState {
+
    type Error = anyhow::Error;
+

+
    fn try_from(context: &Context) -> Result<Self, Self::Error> {
+
        let notifications = inbox::all(context.repository(), context.profile())?;
+
        let mut items = vec![];
+

+
        for notif in &notifications {
+
            if let Ok(notif) = NotificationItem::try_from((context.repository(), notif)) {
+
                items.push(notif);
+
            }
+
        }
+

+
        Ok(Self {
+
            notifications: items,
+
            selected: None,
+
        })
    }
}

pub enum Action {
    Exit,
+
    Select(NotificationId),
}

impl State<Action> for InboxState {
+
    type Exit = Exit<String>;
+

    fn tick(&self) {}

-
    fn handle_action(self, action: Action) -> anyhow::Result<bool> {
+
    fn handle_action(&mut self, action: Action) -> Option<Exit<String>> {
        match action {
-
            Action::Exit => Ok(true),
+
            Action::Select(id) => {
+
                self.selected = Some(id);
+
                None
+
            }
+
            Action::Exit => Some(Exit { value: None }),
        }
    }
}

impl App {
    pub fn new(context: Context, filter: Filter) -> Self {
-
        Self { context, filter }
+
        Self {
+
            context,
+
            _filter: filter,
+
        }
    }

    pub async fn run(&self) -> Result<()> {
        let (terminator, mut interrupt_rx) = termination::create_termination();
        let (store, state_rx) = Store::<Action, InboxState>::new();
        let (frontend, action_rx) = Frontend::<Action>::new();
-
        let state = InboxState {
-
            notifications: vec![],
-
        };
+
        let state = InboxState::try_from(&self.context)?;

        tokio::try_join!(
            store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
modified bin/commands/inbox/select/flux/ui.rs
@@ -2,23 +2,23 @@ use tokio::sync::mpsc::UnboundedSender;

use termion::event::Key;

-
use ratatui::widgets::Paragraph;
-
use ratatui::{backend::Backend, layout::Rect};
+
use ratatui::backend::Backend;
+
use ratatui::layout::{Constraint, Rect};

use radicle_tui as tui;
-
use tui::flux::ui::widget::{Render, Shortcut, ShortcutWidget, ShortcutWidgetProps, Widget};
+

+
use tui::flux::ui::cob::NotificationItem;
+
use tui::flux::ui::widget::{
+
    Render, Shortcut, Shortcuts, ShortcutsProps, Table, TableProps, Widget,
+
};

use crate::tui_inbox::select::flux::{Action, InboxState};

-
pub struct Props {
-
    notifications: Vec<String>,
-
}
+
pub struct ListPageProps {}

-
impl From<&InboxState> for Props {
-
    fn from(state: &InboxState) -> Self {
-
        Props {
-
            notifications: state.notifications().clone(),
-
        }
+
impl From<&InboxState> for ListPageProps {
+
    fn from(_state: &InboxState) -> Self {
+
        Self {}
    }
}

@@ -26,9 +26,11 @@ pub struct ListPage {
    /// Action sender
    pub action_tx: UnboundedSender<Action>,
    // Mapped Props from State
-
    props: Props,
+
    _props: ListPageProps,
+
    /// notification widget
+
    notifications: Notifications,
    /// Shortcut widget
-
    shortcuts: ShortcutWidget<Action>,
+
    shortcuts: Shortcuts<Action>,
}

impl Widget<InboxState, Action> for ListPage {
@@ -38,8 +40,9 @@ impl Widget<InboxState, Action> for ListPage {
    {
        Self {
            action_tx: action_tx.clone(),
-
            props: Props::from(state),
-
            shortcuts: ShortcutWidget::new(state, action_tx.clone()),
+
            _props: ListPageProps::from(state),
+
            notifications: Notifications::new(state, action_tx.clone()),
+
            shortcuts: Shortcuts::new(state, action_tx.clone()),
        }
        .move_with_state(state)
    }
@@ -49,7 +52,9 @@ impl Widget<InboxState, Action> for ListPage {
        Self: Sized,
    {
        ListPage {
-
            props: Props::from(state),
+
            _props: ListPageProps::from(state),
+
            notifications: self.notifications.move_with_state(state),
+
            shortcuts: self.shortcuts.move_with_state(state),
            ..self
        }
    }
@@ -63,26 +68,142 @@ impl Widget<InboxState, Action> for ListPage {
            Key::Char('q') => {
                let _ = self.action_tx.send(Action::Exit);
            }
-
            _ => {}
+
            _ => {
+
                <Notifications as Widget<InboxState, Action>>::handle_key_event(
+
                    &mut self.notifications,
+
                    key,
+
                );
+
            }
        }
    }
}

impl Render<()> for ListPage {
-
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, _area: Rect, _props: ()) {
+
    fn render<B: Backend>(&mut self, frame: &mut ratatui::Frame, _area: Rect, _props: ()) {
        let area = frame.size();
        let layout = tui::flux::ui::layout::default_page(area, 1u16, 1u16);

+
        self.notifications.render::<B>(frame, layout.component, ());
        self.shortcuts.render::<B>(
            frame,
            layout.shortcuts,
-
            ShortcutWidgetProps {
-
                shortcuts: vec![Shortcut::new("q", "quit"), Shortcut::new("s", "show"), Shortcut::new("?", "help")],
+
            ShortcutsProps {
+
                shortcuts: vec![Shortcut::new("enter", "select"), Shortcut::new("q", "quit")],
                divider: '∙',
            },
        );
+
    }
+
}
+

+
struct NotificationsProps {
+
    notifications: Vec<NotificationItem>,
+
}
+

+
impl From<&InboxState> for NotificationsProps {
+
    fn from(state: &InboxState) -> Self {
+
        Self {
+
            notifications: state.notifications.clone(),
+
        }
+
    }
+
}
+

+
struct Notifications {
+
    /// Sending actions to the state store
+
    action_tx: UnboundedSender<Action>,
+
    /// State Mapped RoomList Props
+
    props: NotificationsProps,
+
    /// Notification table
+
    table: Table<Action, NotificationItem, 7>,
+
}
+

+
impl Widget<InboxState, Action> for Notifications {
+
    fn new(state: &InboxState, action_tx: UnboundedSender<Action>) -> Self {
+
        Self {
+
            action_tx: action_tx.clone(),
+
            props: NotificationsProps::from(state),
+
            table: Table::new(state, action_tx.clone()),
+
        }
+
    }
+

+
    fn move_with_state(self, state: &InboxState) -> Self
+
    where
+
        Self: Sized,
+
    {
+
        Self {
+
            props: NotificationsProps::from(state),
+
            table: self.table.move_with_state(state),
+
            ..self
+
        }
+
    }
+

+
    fn name(&self) -> &str {
+
        "notification-list"
+
    }
+

+
    fn handle_key_event(&mut self, key: Key) {
+
        match key {
+
            Key::Up => {
+
                self.table.prev();
+

+
                let selected = self
+
                    .table
+
                    .selected()
+
                    .and_then(|selected| self.props.notifications.get(selected));
+

+
                // TODO: propagate error
+
                if let Some(notif) = selected {
+
                    let _ = self.action_tx.send(Action::Select(notif.id));
+
                }
+
            }
+
            Key::Down => {
+
                self.table.next(self.props.notifications.len());
+

+
                let selected = self
+
                    .table
+
                    .selected()
+
                    .and_then(|selected| self.props.notifications.get(selected));

-
        // let shortcuts = Paragraph::new(String::from("q quit"));
-
        // frame.render_widget(self.shortcuts, layout.shortcuts);
+
                // TODO: propagate error
+
                if let Some(notif) = selected {
+
                    let _ = self.action_tx.send(Action::Select(notif.id));
+
                }
+
            }
+
            _ => {}
+
        }
+
    }
+
}
+

+
impl Render<()> for Notifications {
+
    fn render<B: Backend>(&mut self, frame: &mut ratatui::Frame, area: Rect, _props: ()) {
+
        let header = [
+
            String::from(""),
+
            String::from(" ● "),
+
            String::from("Type"),
+
            String::from("Summary"),
+
            String::from("ID"),
+
            String::from("Status"),
+
            String::from("Updated"),
+
        ];
+

+
        let widths = [
+
            Constraint::Length(5),
+
            Constraint::Length(3),
+
            Constraint::Length(6),
+
            Constraint::Fill(1),
+
            Constraint::Length(15),
+
            Constraint::Length(10),
+
            Constraint::Length(18),
+
        ];
+

+
        self.table.render::<B>(
+
            frame,
+
            area,
+
            TableProps {
+
                items: self.props.notifications.to_vec(),
+
                header,
+
                widths: widths.to_vec(),
+
                focus: false,
+
            },
+
        );
    }
}
modified bin/commands/inbox/select/realm.rs
@@ -17,7 +17,7 @@ use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause};

use radicle_tui as tui;

-
use tui::cob::inbox::Filter;
+
use tui::cob::inbox::{Filter, SortBy};
use tui::context::Context;
use tui::ui::subscription;
use tui::ui::theme::Theme;
@@ -111,8 +111,10 @@ pub struct App {
    context: Context,
    pages: PageStack<Cid, Message>,
    theme: Theme,
-
    quit: bool,
+
    mode: Mode,
    filter: Filter,
+
    sort_by: SortBy,
+
    quit: bool,
    output: Option<SelectionExit>,
}

@@ -120,13 +122,15 @@ pub struct App {
/// components and sets focus to a default one.
#[allow(dead_code)]
impl App {
-
    pub fn new(context: Context, filter: Filter) -> Self {
+
    pub fn new(context: Context, mode: Mode, filter: Filter, sort_by: SortBy) -> Self {
        Self {
            context,
            pages: PageStack::default(),
            theme: Theme::default(),
-
            quit: false,
+
            mode,
            filter,
+
            sort_by,
+
            quit: false,
            output: None,
        }
    }
@@ -136,7 +140,11 @@ impl App {
        app: &mut Application<Cid, Message, NoUserEvent>,
        theme: &Theme,
    ) -> Result<()> {
-
        let home = Box::new(ListView::new(self.filter.clone()));
+
        let home = Box::new(ListView::new(
+
            self.mode.clone(),
+
            self.filter.clone(),
+
            self.sort_by,
+
        ));
        self.pages.push(home, app, &self.context, theme)?;

        Ok(())
modified bin/commands/inbox/select/realm/ui.rs
@@ -49,7 +49,7 @@ impl NotificationBrowser {
            ColumnWidth::Fixed(10),
            ColumnWidth::Fixed(18),
        ];
-
        
+

        let mut items = vec![];
        for notification in context.notifications() {
            if let Ok(item) =