koibumi 0.0.9

An experimental Bitmessage client
// Based on: Iced stopwatch example
// https://github.com/hecrj/iced/blob/master/examples/stopwatch/src/main.rs

use std::{
    cell::RefCell,
    sync::atomic::{AtomicUsize, Ordering},
};

use async_std::task;
use futures::{
    channel::mpsc::{Receiver, SendError, Sender},
    sink::SinkExt,
    stream::StreamExt,
};
use iced::{button, Application, Button, Column, Command, Element, Subscription, Text};
use log::{error, info};

use koibumi_common::{
    boxes::{Boxes, DEFAULT_USER_ID},
    param::Params,
};
use koibumi_core::{address::Address, message};
use koibumi_node::{self as node, Command as NodeCommand, Event as BmEvent, Response};

use crate::{
    bridge,
    config::Config as GuiConfig,
    contacts, identities, messages, send, settings, status, style, subscriptions,
    tab::{Tab, Tabs},
};

const TITLE: &str = "Koibumi - An Experimental Bitmessage Client";

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum State {
    Idle,
    Running,
    Stopping,
}

pub struct Gui {
    params: Params,
    boxes: Option<Boxes>,

    command_sender: Sender<NodeCommand>,
    node_handle: task::JoinHandle<()>,
    response_receiver: Receiver<Response>,
    bm_event_receiver: RefCell<Option<Receiver<BmEvent>>>,
    count: AtomicUsize,

    state: State,

    config: GuiConfig,

    start_button: button::State,

    tabs: Tabs,
    tab: Tab,

    messages_tab: messages::Tab,
    send_tab: send::Tab,
    identities_tab: identities::Tab,
    contacts_tab: contacts::Tab,
    subscriptions_tab: subscriptions::Tab,
    settings_tab: settings::Tab,
    status_tab: status::Tab,

    logger: crate::log::Logger,
}

#[derive(Clone, Debug)]
pub enum Message {
    StartButtonPushed,
    StopSent(Result<(), SendError>),
    AbortSent(Result<(), SendError>),
    BmEvent(BmEvent),

    TabSelected(Tab),

    MessagesMessage(messages::Message),
    SendMessage(send::Message),
    IdentitiesMessage(identities::Message),
    ContactsMessage(contacts::Message),
    SubscriptionsMessage(subscriptions::Message),
    SettingsMessage(settings::Message),
}

async fn send_stop(mut sender: Sender<NodeCommand>) -> Result<(), SendError> {
    sender.send(NodeCommand::Stop).await
}

async fn send_abort(mut sender: Sender<NodeCommand>) -> Result<(), SendError> {
    sender.send(NodeCommand::Abort).await
}

impl Gui {
    fn handle_msg(&mut self, user_id: Vec<u8>, address: Address, object: message::Object) {
        task::block_on(async {
            if let Some(boxes) = &mut self.boxes {
                let identity = boxes.user().private_identity_by_address(&address);
                if identity.is_none() {
                    error!("identity not found for address: {}", address);
                    return;
                }
                let identity = identity.unwrap();
                match boxes.manager().insert_msg(user_id, identity, object).await {
                    Ok(message) => {
                        let entry = messages::Entry::new(koibumi_box::MessageEntry::from(&message));
                        self.messages_tab.entries.insert(0, entry);
                        boxes.increment_unread_count();
                    }
                    Err(err) => {
                        error!("{}", err);
                        return;
                    }
                }
            }
        });
    }

    fn handle_broadcast(&mut self, user_id: Vec<u8>, address: Address, object: message::Object) {
        task::block_on(async {
            if let Some(boxes) = &mut self.boxes {
                match boxes
                    .manager()
                    .insert_broadcast(user_id, address, object)
                    .await
                {
                    Ok(message) => {
                        let entry = messages::Entry::new(koibumi_box::MessageEntry::from(&message));
                        self.messages_tab.entries.insert(0, entry);
                        boxes.increment_unread_count();
                    }
                    Err(err) => {
                        error!("{}", err);
                        return;
                    }
                }
            }
        });
    }
}

impl Application for Gui {
    type Executor = iced::executor::Default;
    type Message = Message;
    type Flags = ();

    fn new(_flags: ()) -> (Self, Command<Message>) {
        let params = Params::new();

        koibumi_common::log::init(&params).unwrap_or_else(|err| {
            println!("Warning: Failed to initialize logger.");
            println!("{}", err);
        });

        let config = koibumi_common::config::load(&params).unwrap_or_else(|err| {
            error!("Failed to load config file: {}", err);
            std::process::exit(1)
        });

        let mut boxes = match task::block_on(koibumi_common::boxes::prepare(&params)) {
            Ok(boxes) => Some(boxes),
            Err(err) => {
                error!("{}", err);
                None
            }
        };

        let mut messages_tab = messages::Tab::default();
        messages_tab.entries = if let Some(boxes) = &mut boxes {
            let entries =
                task::block_on(async { boxes.manager().message_list(DEFAULT_USER_ID).await });
            if let Err(err) = entries {
                error!("{}", err);
                Vec::new()
            } else {
                let mut list = Vec::new();
                let mut count = 0;
                for entry in entries.unwrap() {
                    if !entry.read() {
                        count += 1;
                    }
                    list.push(messages::Entry::new(entry));
                }
                boxes.set_unread_count(count);
                list
            }
        } else {
            Vec::new()
        };

        let (command_sender, response_receiver, node_handle) = node::spawn();

        (
            Self {
                params,
                boxes,

                command_sender,
                node_handle,
                response_receiver,
                bm_event_receiver: RefCell::new(None),
                count: AtomicUsize::new(0),

                state: State::Idle,

                config: GuiConfig::default(),

                start_button: button::State::new(),

                tabs: Tabs::default(),
                tab: Tab::default(),

                messages_tab,
                send_tab: send::Tab::default(),
                identities_tab: identities::Tab::default(),
                contacts_tab: contacts::Tab::default(),
                subscriptions_tab: subscriptions::Tab::default(),
                settings_tab: settings::Tab::new(&config),
                status_tab: status::Tab::default(),

                logger: crate::log::Logger::default(),
            },
            Command::none(),
        )
    }

    fn title(&self) -> String {
        if let Some(boxes) = &self.boxes {
            let count = boxes.unread_count();
            if count > 0 {
                format!("({}) {}", count, TITLE)
            } else {
                TITLE.to_string()
            }
        } else {
            format!("[No boxes] {}", TITLE)
        }
    }

    #[allow(clippy::cognitive_complexity)]
    fn update(&mut self, message: Message) -> Command<Message> {
        match message {
            Message::StartButtonPushed => match self.state {
                State::Idle => {
                    info!("Start");
                    self.logger.info("Start");

                    if self.boxes.is_none() {
                        error!("No boxes");
                        self.logger.error("No boxes");
                        return Command::none();
                    }

                    let config = self.settings_tab.create_config();

                    self.settings_tab.seeds_value = config
                        .seeds()
                        .iter()
                        .map(|v| v.to_string())
                        .collect::<Vec<String>>()
                        .join(" ");
                    self.settings_tab.max_outgoing_initiated_value =
                        config.max_outgoing_initiated().to_string();
                    self.settings_tab.max_outgoing_established_value =
                        config.max_outgoing_established().to_string();
                    self.settings_tab.own_nodes_value = config
                        .own_nodes()
                        .iter()
                        .map(|v| v.to_string())
                        .collect::<Vec<String>>()
                        .join(" ");

                    if config != self.settings_tab.config {
                        self.settings_tab.config = config.clone();
                        if let Err(err) = koibumi_common::config::save(&self.params, &config) {
                            error!("{}", err);
                            self.logger.error("Could not save config file");
                        }
                    }

                    let mut sender = self.command_sender.clone();
                    let response = task::block_on(async {
                        let pool = koibumi_common::node::prepare(&self.params).await;
                        if let Err(err) = pool {
                            error!("{}", err);
                            self.logger.error("Could not prepare node");
                            return None;
                        }
                        let pool = pool.unwrap();

                        let users = vec![self.boxes.as_ref().unwrap().user().clone().into()];
                        if let Err(err) = sender
                            .send(NodeCommand::Start(config.into(), pool, users))
                            .await
                        {
                            error!("{}", err);
                            self.logger.error("Could not start node");
                            return None;
                        }
                        self.response_receiver.next().await
                    });
                    if let Some(response) = response {
                        let Response::Started(receiver) = response;
                        self.bm_event_receiver.replace(Some(receiver));
                    } else {
                        return Command::none();
                    }

                    self.state = State::Running;
                    Command::none()
                }
                State::Running => {
                    info!("Stop");
                    self.logger.info("Stop");
                    self.state = State::Stopping;
                    Command::perform(send_stop(self.command_sender.clone()), Message::StopSent)
                }
                State::Stopping => {
                    info!("Abort");
                    self.logger.info("Abort");
                    Command::perform(send_abort(self.command_sender.clone()), Message::AbortSent)
                }
            },
            Message::StopSent(_) => Command::none(),
            Message::AbortSent(_) => Command::none(),
            Message::BmEvent(bm_event) => {
                match bm_event {
                    BmEvent::ConnectionCounts {
                        incoming_initiated,
                        incoming_connected,
                        incoming_established,
                        outgoing_initiated,
                        outgoing_connected,
                        outgoing_established,
                    } => {
                        self.status_tab.incoming_initiated = incoming_initiated;
                        self.status_tab.incoming_connected = incoming_connected;
                        self.status_tab.incoming_established = incoming_established;
                        self.status_tab.outgoing_initiated = outgoing_initiated;
                        self.status_tab.outgoing_connected = outgoing_connected;
                        self.status_tab.outgoing_established = outgoing_established;
                    }
                    BmEvent::AddrCount(count) => {
                        self.status_tab.addr_count = count;
                    }
                    BmEvent::Established {
                        addr,
                        user_agent,
                        rating,
                    } => {
                        self.status_tab.peers.push(addr.clone());
                        self.status_tab
                            .peer_infos
                            .insert(addr, (user_agent, rating));
                    }
                    BmEvent::Disconnected { addr } => {
                        if let Some(index) = self.status_tab.peers.iter().position(|v| v == &addr) {
                            self.status_tab.peers.remove(index);
                        }
                    }
                    BmEvent::Objects {
                        missing,
                        loaded,
                        uploaded,
                    } => {
                        self.status_tab.missing_objects = missing;
                        self.status_tab.loaded_objects = loaded;
                        self.status_tab.uploaded_objects = uploaded;
                    }
                    BmEvent::Stopped => {
                        self.state = State::Idle;
                    }
                    BmEvent::Msg {
                        user_id,
                        address,
                        object,
                    } => {
                        self.handle_msg(user_id, address, object);
                    }
                    BmEvent::Broadcast {
                        user_id,
                        address,
                        object,
                    } => {
                        self.handle_broadcast(user_id, address, object);
                    }
                }
                Command::none()
            }

            Message::TabSelected(tab) => {
                self.tab = tab;
                Command::none()
            }

            Message::MessagesMessage(message) => self.messages_tab.update(message, &mut self.boxes),
            Message::SendMessage(message) => self.send_tab.update(
                message,
                self.state,
                &mut self.boxes,
                &mut self.command_sender,
                &mut self.logger,
            ),
            Message::IdentitiesMessage(message) => self.identities_tab.update(
                message,
                &mut self.boxes,
                &mut self.command_sender,
                &mut self.logger,
            ),
            Message::ContactsMessage(message) => {
                self.contacts_tab
                    .update(message, &mut self.boxes, &mut self.command_sender)
            }
            Message::SubscriptionsMessage(message) => {
                self.subscriptions_tab
                    .update(message, &mut self.boxes, &mut self.command_sender)
            }
            Message::SettingsMessage(message) => self.settings_tab.update(message, self.state),
        }
    }

    fn view(&mut self) -> Element<Message> {
        let text_size = self.config.text_size();

        let button = |state, label, style| {
            Button::new(state, Text::new(label).size(text_size))
                .padding(text_size / 2)
                .style(style)
        };

        let start_button = {
            let (label, color) = match self.state {
                State::Idle => ("Start", style::StartButton::Start),
                State::Running => ("Stop", style::StartButton::Stop),
                State::Stopping => ("Abort", style::StartButton::Abort),
            };

            button(&mut self.start_button, label, color).on_press(Message::StartButtonPushed)
        };

        let column = Column::new()
            .padding(text_size / 4)
            .spacing(text_size / 4)
            .push(start_button)
            .push(self.tabs.view(&self.config, self.tab))
            .push(match self.tab {
                Tab::Messages => self.messages_tab.view(&self.config, &self.boxes),
                Tab::Send => self.send_tab.view(&self.config, self.state, &self.boxes),
                Tab::Identities => {
                    let identities = if let Some(boxes) = &self.boxes {
                        boxes.user().private_identities().to_vec()
                    } else {
                        Vec::new()
                    };
                    let selected_index = if let Some(boxes) = &self.boxes {
                        boxes.selected_identity_index()
                    } else {
                        None
                    };
                    self.identities_tab
                        .view(&self.config, &identities, selected_index, &self.boxes)
                }
                Tab::Contacts => {
                    let contacts = if let Some(boxes) = &self.boxes {
                        boxes.user().contacts().to_vec()
                    } else {
                        Vec::new()
                    };
                    let selected_index = if let Some(boxes) = &self.boxes {
                        boxes.selected_contact_index()
                    } else {
                        None
                    };
                    self.contacts_tab
                        .view(&self.config, &contacts, selected_index, &self.boxes)
                }
                Tab::Subscriptions => {
                    let subscriptions = if let Some(boxes) = &self.boxes {
                        boxes.user().subscriptions().to_vec()
                    } else {
                        Vec::new()
                    };
                    self.subscriptions_tab
                        .view(&self.config, &subscriptions, &self.boxes)
                }
                Tab::Settings => self.settings_tab.view(&self.config),
                Tab::Status => self.status_tab.view(&self.config),
                Tab::Log => self.logger.tab.view(&self.config),
            });
        column.push(self.logger.bar.view(&self.config)).into()
    }

    fn subscription(&self) -> Subscription<Message> {
        match self.state {
            State::Idle => Subscription::none(),
            State::Running | State::Stopping => {
                if let Some(receiver) = self.bm_event_receiver.replace(None) {
                    // supply real stream and fresh id to generate distinct hash
                    let id = self.count.fetch_add(1, Ordering::SeqCst);
                    bridge::bridge(receiver, id).map(Message::BmEvent)
                } else {
                    // supply dummy stream and active id to keep current hash
                    let id = self.count.load(Ordering::SeqCst) - 1;
                    bridge::keep_alive(id).map(Message::BmEvent)
                }
            }
        }
    }
}