Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
bin/inbox: Load notifications async
Erik Kundt committed 6 months ago
commit c66a399c8ae426654b22dda9351337cd9b25c0e0
parent 904586f8c6a83b9f19b429c54e43f1c2b2067825
5 files changed +156 -113
modified bin/commands/inbox.rs
@@ -7,6 +7,8 @@ use std::ffi::OsString;

use anyhow::anyhow;

+
use radicle::storage::{HasRepoId, ReadRepository};
+

use radicle_cli::terminal;
use radicle_cli::terminal::{Args, Error, Help};

@@ -207,12 +209,13 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul

            let context = list::Context {
                profile,
-
                repository,
+
                project: repository.identity_doc()?.project()?,
+
                rid: repository.rid(),
                mode: opts.mode,
                filter: opts.filter.clone(),
                sort_by: opts.sort_by,
            };
-
            let selection = list::App::new(context).run().await?;
+
            let selection = list::App::default().run(context).await?;

            if opts.json {
                let selection = selection
modified bin/commands/inbox/list.rs
@@ -2,19 +2,23 @@
mod ui;

use std::str::FromStr;
+
use std::sync::Arc;
+
use std::sync::Mutex;

use anyhow::Result;

-
use ratatui::Viewport;
use termion::event::Key;

+
use tokio::sync::mpsc::UnboundedSender;
+

use ratatui::layout::Constraint;
use ratatui::style::Stylize;
use ratatui::text::Text;
+
use ratatui::Viewport;

use radicle::identity::Project;
use radicle::node::notifications::NotificationId;
-
use radicle::storage::git::Repository;
+
use radicle::prelude::RepoId;
use radicle::storage::ReadRepository;
use radicle::storage::ReadStorage;
use radicle::Profile;
@@ -46,18 +50,127 @@ use super::common::{Mode, RepositoryMode};

type Selection = tui::Selection<NotificationId>;

+
#[derive(Clone, Debug)]
+
struct Worker {
+
    sender: UnboundedSender<Message>,
+
}
+

+
impl Worker {
+
    fn load_notifications(&self, context: Arc<Mutex<Context>>) {
+
        let tx = self.sender.clone();
+
        tokio::spawn(async move {
+
            let context = context.lock().unwrap();
+

+
            let mut notifications = match context.mode.repository() {
+
                RepositoryMode::All => {
+
                    let mut repos = context.profile.storage.repositories()?;
+
                    repos.sort_by_key(|r| r.rid);
+

+
                    let mut notifs = vec![];
+
                    for repo in repos {
+
                        let repo = context.profile.storage.repository(repo.rid)?;
+

+
                        let items = inbox::all(&repo, &context.profile)?
+
                            .iter()
+
                            .map(|notif| {
+
                                Notification::new(&context.profile, &context.project, &repo, notif)
+
                            })
+
                            .filter_map(|item| item.ok())
+
                            .flatten()
+
                            .collect::<Vec<_>>();
+

+
                        notifs.extend(items);
+
                    }
+

+
                    notifs
+
                }
+
                RepositoryMode::Contextual => {
+
                    let repository = context.profile.storage.repository(context.rid)?;
+
                    let notifs = inbox::all(&repository, &context.profile)?;
+

+
                    notifs
+
                        .iter()
+
                        .map(|notif| {
+
                            Notification::new(
+
                                &context.profile,
+
                                &context.project,
+
                                &repository,
+
                                notif,
+
                            )
+
                        })
+
                        .filter_map(|item| item.ok())
+
                        .flatten()
+
                        .collect::<Vec<_>>()
+
                }
+
                RepositoryMode::ByRepo((rid, _)) => {
+
                    let repo = context.profile.storage.repository(*rid)?;
+
                    let notifs = inbox::all(&repo, &context.profile)?;
+

+
                    notifs
+
                        .iter()
+
                        .map(|notif| {
+
                            Notification::new(&context.profile, &context.project, &repo, notif)
+
                        })
+
                        .filter_map(|item| item.ok())
+
                        .flatten()
+
                        .collect::<Vec<_>>()
+
                }
+
            };
+

+
            // Set project name
+
            let mode = match &context.mode.repository() {
+
                RepositoryMode::ByRepo((rid, _)) => {
+
                    let project = context
+
                        .profile
+
                        .storage
+
                        .repository(*rid)?
+
                        .identity_doc()?
+
                        .project()?;
+
                    let name = project.name().to_string();
+

+
                    context
+
                        .mode
+
                        .clone()
+
                        .with_repository(RepositoryMode::ByRepo((*rid, Some(name))))
+
                }
+
                _ => context.mode.clone(),
+
            };
+

+
            // Apply sorting
+
            // match context.sort_by.field {
+
            //     "timestamp" => notifications.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)),
+
            //     "id" => notifications.sort_by(|a, b| a.id.cmp(&b.id)),
+
            //     _ => {}
+
            // }
+
            // if context.sort_by.reverse {
+
            //     notifications.reverse();
+
            // }
+

+
            // Sort by project if all notifications are shown
+
            if let RepositoryMode::All = mode.repository() {
+
                notifications.sort_by(|a, b| a.project.cmp(&b.project));
+
            }
+

+
            let _ = tx.send(Message::NotificationsLoaded(notifications));
+

+
            anyhow::Ok(())
+
        });
+
    }
+
}
+

#[allow(dead_code)]
+
#[derive(Clone, Debug)]
pub struct Context {
    pub profile: Profile,
-
    pub repository: Repository,
+
    pub project: Project,
+
    pub rid: RepoId,
    pub mode: Mode,
    pub filter: NotificationFilter,
    pub sort_by: inbox::SortBy,
}

-
pub struct App {
-
    context: Context,
-
}
+
#[derive(Default)]
+
pub struct App {}

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum AppPage {
@@ -91,108 +204,21 @@ pub struct HelpState {

#[derive(Clone, Debug)]
pub struct State {
-
    mode: Mode,
-
    project: Project,
    pages: PageStack<AppPage>,
    browser: BrowserState,
    help: HelpState,
+
    context: Arc<Mutex<Context>>,
+
    worker: Arc<Mutex<Worker>>,
}

-
impl TryFrom<&Context> for State {
-
    type Error = anyhow::Error;
-

-
    fn try_from(context: &Context) -> Result<Self, Self::Error> {
-
        let doc = context.repository.identity_doc()?;
-
        let project = doc.project()?;
-

+
impl State {
+
    fn new(context: Context, sender: UnboundedSender<Message>) -> Result<Self, anyhow::Error> {
        let search = BufferedValue::new(context.filter.to_string());

-
        let mut notifications = match &context.mode.repository() {
-
            RepositoryMode::All => {
-
                let mut repos = context.profile.storage.repositories()?;
-
                repos.sort_by_key(|r| r.rid);
-

-
                let mut notifs = vec![];
-
                for repo in repos {
-
                    let repo = context.profile.storage.repository(repo.rid)?;
-

-
                    let items = inbox::all(&repo, &context.profile)?
-
                        .iter()
-
                        .map(|notif| Notification::new(&context.profile, &project, &repo, notif))
-
                        .filter_map(|item| item.ok())
-
                        .flatten()
-
                        .collect::<Vec<_>>();
-

-
                    notifs.extend(items);
-
                }
-

-
                notifs
-
            }
-
            RepositoryMode::Contextual => {
-
                let notifs = inbox::all(&context.repository, &context.profile)?;
-

-
                notifs
-
                    .iter()
-
                    .map(|notif| {
-
                        Notification::new(&context.profile, &project, &context.repository, notif)
-
                    })
-
                    .filter_map(|item| item.ok())
-
                    .flatten()
-
                    .collect::<Vec<_>>()
-
            }
-
            RepositoryMode::ByRepo((rid, _)) => {
-
                let repo = context.profile.storage.repository(*rid)?;
-
                let notifs = inbox::all(&repo, &context.profile)?;
-

-
                notifs
-
                    .iter()
-
                    .map(|notif| Notification::new(&context.profile, &project, &repo, notif))
-
                    .filter_map(|item| item.ok())
-
                    .flatten()
-
                    .collect::<Vec<_>>()
-
            }
-
        };
-

-
        // Set project name
-
        let mode = match &context.mode.repository() {
-
            RepositoryMode::ByRepo((rid, _)) => {
-
                let project = context
-
                    .profile
-
                    .storage
-
                    .repository(*rid)?
-
                    .identity_doc()?
-
                    .project()?;
-
                let name = project.name().to_string();
-

-
                context
-
                    .mode
-
                    .clone()
-
                    .with_repository(RepositoryMode::ByRepo((*rid, Some(name))))
-
            }
-
            _ => context.mode.clone(),
-
        };
-

-
        // Apply sorting
-
        match context.sort_by.field {
-
            "timestamp" => notifications.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)),
-
            "id" => notifications.sort_by(|a, b| a.id.cmp(&b.id)),
-
            _ => {}
-
        }
-
        if context.sort_by.reverse {
-
            notifications.reverse();
-
        }
-

-
        // Sort by project if all notifications are shown
-
        if let RepositoryMode::All = mode.repository() {
-
            notifications.sort_by(|a, b| a.project.cmp(&b.project));
-
        }
-

        Ok(Self {
-
            mode: context.mode.clone(),
-
            project,
            pages: PageStack::new(vec![AppPage::Browse]),
            browser: BrowserState {
-
                items: notifications,
+
                items: vec![],
                selected: Some(0),
                filter: context.filter.clone(),
                search,
@@ -201,6 +227,8 @@ impl TryFrom<&Context> for State {
            help: HelpState {
                text: TextViewState::default().content(help_text()),
            },
+
            worker: Arc::new(Mutex::new(Worker { sender })),
+
            context: Arc::new(Mutex::new(context)),
        })
    }
}
@@ -216,6 +244,8 @@ pub enum Message {
    OpenHelp,
    LeavePage,
    ScrollHelp { state: TextViewState },
+
    Reload,
+
    NotificationsLoaded(Vec<Notification>),
}

impl store::Update<Message> for State {
@@ -270,18 +300,24 @@ impl store::Update<Message> for State {
                self.help.text = state;
                None
            }
+
            Message::Reload => {
+
                if let Ok(worker) = self.worker.lock() {
+
                    worker.load_notifications(self.context.clone());
+
                }
+
                None
+
            }
+
            Message::NotificationsLoaded(notifs) => {
+
                self.browser.items = notifs;
+
                None
+
            }
        }
    }
}

impl App {
-
    pub fn new(context: Context) -> Self {
-
        Self { context }
-
    }
-

-
    pub async fn run(&self) -> Result<Option<Selection>> {
+
    pub async fn run(&self, context: Context) -> Result<Option<Selection>> {
        let channel = Channel::default();
-
        let state = State::try_from(&self.context)?;
+
        let state = State::new(context, channel.tx.clone())?;
        let tx = channel.tx.clone();

        let window = Window::default()
@@ -294,6 +330,7 @@ impl App {
                    .to_boxed_any()
                    .into()
            });
+
        channel.tx.send(Message::Reload)?;

        tui::rm(state, window, Viewport::Inline(20), channel).await
    }
@@ -309,10 +346,11 @@ fn browser_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Mes
    let shortcuts = Shortcuts::default()
        .to_widget(tx.clone())
        .on_update(|state: &State| {
+
            let context = state.context.lock().unwrap();
            let shortcuts = if state.browser.show_search {
                vec![("esc", "cancel"), ("enter", "apply")]
            } else {
-
                match state.mode.selection() {
+
                match context.mode.selection() {
                    SelectionMode::Id => vec![("enter", "select"), ("/", "search")],
                    SelectionMode::Operation => vec![
                        ("enter", "show"),
modified bin/commands/inbox/list/ui.rs
@@ -54,8 +54,10 @@ pub struct BrowserProps<'a> {

impl From<&State> for BrowserProps<'_> {
    fn from(state: &State) -> Self {
-
        let header = match state.mode.repository() {
-
            RepositoryMode::Contextual => state.project.name().to_string(),
+
        let context = state.context.lock().unwrap();
+

+
        let header = match context.mode.repository() {
+
            RepositoryMode::Contextual => context.project.name().to_string(),
            RepositoryMode::All => "All repositories".to_string(),
            RepositoryMode::ByRepo((_, name)) => name.clone().unwrap_or_default(),
        };
@@ -75,7 +77,7 @@ impl From<&State> for BrowserProps<'_> {
        let stats = HashMap::from([("Seen".to_string(), seen), ("Unseen".to_string(), unseen)]);

        Self {
-
            mode: state.mode.clone(),
+
            mode: context.mode.clone(),
            header,
            notifications,
            selected: state.browser.selected,
@@ -85,7 +87,7 @@ impl From<&State> for BrowserProps<'_> {
                Column::new("", Constraint::Length(3)),
                Column::new("", Constraint::Fill(5)),
                Column::new("", Constraint::Fill(1))
-
                    .skip(*state.mode.repository() != RepositoryMode::All),
+
                    .skip(*context.mode.repository() != RepositoryMode::All),
                Column::new("", Constraint::Fill(1))
                    .hide_small()
                    .hide_medium(),
modified bin/commands/issue/list.rs
@@ -6,12 +6,12 @@ use std::str::FromStr;

use anyhow::{bail, Result};

-
use ratatui::Viewport;
use termion::event::Key;

use ratatui::layout::Constraint;
use ratatui::style::Stylize;
use ratatui::text::Text;
+
use ratatui::Viewport;

use radicle::cob::thread::CommentId;
use radicle::git::Oid;
modified bin/commands/patch/list.rs
@@ -7,7 +7,6 @@ use std::str::FromStr;

use anyhow::Result;

-
use ratatui::Viewport;
use termion::event::Key;

use radicle_tui as tui;
@@ -15,6 +14,7 @@ use radicle_tui as tui;
use ratatui::layout::Constraint;
use ratatui::style::Stylize;
use ratatui::text::Text;
+
use ratatui::Viewport;

use tui::store;
use tui::ui::rm::widget::container::{Container, Footer, FooterProps, Header, HeaderProps};