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(¶ms).unwrap_or_else(|err| {
println!("Warning: Failed to initialize logger.");
println!("{}", err);
});
let config = koibumi_common::config::load(¶ms).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(¶ms)) {
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) {
let id = self.count.fetch_add(1, Ordering::SeqCst);
bridge::bridge(receiver, id).map(Message::BmEvent)
} else {
let id = self.count.load(Ordering::SeqCst) - 1;
bridge::keep_alive(id).map(Message::BmEvent)
}
}
}
}
}