koibumi-sync 0.0.1

An experimental Bitmessage client (sync version)
use std::convert::TryFrom;

use copypasta::ClipboardProvider;
use log::error;

use koibumi_common_sync::boxes::{Boxes, DEFAULT_USER_ID};
use koibumi_core::{
    encoding::{Encoding, Simple},
    message::InvHash,
    time::Time,
};

use crate::{config::Config as GuiConfig, ids::Ids};

#[derive(Clone, Debug)]
pub(crate) struct Entry {
    entry: koibumi_box_sync::MessageEntry,
}

impl Entry {
    pub(crate) fn new(entry: koibumi_box_sync::MessageEntry) -> Self {
        Self { entry }
    }
}

#[derive(Clone, Debug, Default)]
pub(crate) struct Tab {
    pub(crate) entries: Vec<Entry>,
    selected: Option<InvHash>,
    message: Option<koibumi_box_sync::Message>,
}

fn time_to_string(time: Time) -> String {
    if time.as_secs() > i64::MAX as u64 {
        return "(unsupported)".to_string();
    }
    let time = ::time::OffsetDateTime::from_unix_timestamp(time.as_secs() as i64);
    if time.is_err() {
        return "(error)".to_string();
    }
    let mut time = time.unwrap();
    if let Ok(local_offset) = ::time::UtcOffset::local_offset_at(time) {
        time = time.to_offset(local_offset);
    }
    let format = time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory]:[offset_minute]").unwrap();
    if let Ok(s) = time.format(&format) {
        s
    } else {
        "(error)".to_string()
    }
}

impl Tab {
    // Draw the Ui.
    pub(crate) fn set_widgets(
        &mut self,
        ui: &mut conrod_core::UiCell,
        ids: &mut Ids,
        config: &GuiConfig,
        boxes: &mut Option<Boxes>,
    ) {
        let text_size = config.text_size();
        let font_id = ui.fonts.ids().next().unwrap();

        if boxes.is_none() {
            return;
        }
        let boxes = boxes.as_mut().unwrap();

        use conrod_core::{
            color,
            position::{Place, Relative},
            text, widget, Colorable, Labelable, Positionable, Sizeable, Widget,
        };

        widget::Canvas::new()
            .flow_down(&[
                (
                    ids.messages_list_canvas,
                    widget::Canvas::new().length(128.0).pad_bottom(2.0),
                ),
                (
                    ids.messages_buttons_canvas,
                    widget::Canvas::new()
                        .length(text_size as f64 + 8.0 + 2.0 + 2.0)
                        .pad_bottom(2.0),
                ),
                (
                    ids.messages_header_canvas,
                    widget::Canvas::new()
                        .length(text_size as f64 / 2.0 * 13.0)
                        .scroll_kids_vertically()
                        .color(color::DARK_CHARCOAL)
                        .pad_bottom(2.0),
                ),
                (
                    ids.messages_body_canvas,
                    widget::Canvas::new()
                        .scroll_kids_vertically()
                        .pad_bottom(2.0),
                ),
            ])
            .top_left_of(ids.tabs_body)
            .wh_of(ids.tabs_body)
            .set(ids.messages, ui);

        let (mut items, scrollbar) = widget::List::flow_down(self.entries.len())
            .top_left_of(ids.messages_list_canvas)
            .wh_of(ids.messages_list_canvas)
            .item_size(text_size as f64 + 8.0)
            .scrollbar_on_top()
            .set(ids.messages_list, ui);

        while let Some(item) = items.next(ui) {
            let i = item.i;
            let label = self.entries[i].entry.subject();
            let mut button = widget::Button::new()
                .label(&label)
                .label_color(color::WHITE)
                .label_font_size(text_size as u32)
                .label_x(Relative::Place(Place::Start(Some(2.0))));
            if self.entries[i].entry.read() {
                button = button.color(color::DARK_CHARCOAL);
            } else {
                button = button.color(color::DARK_YELLOW);
            }
            if let Some(hash) = &self.selected {
                if self.entries[i].entry.hash() == hash {
                    button = button.color(color::GREEN);
                }
            }
            for _v in item.set(button, ui) {
                let hash = self.entries[i].entry.hash().clone();

                let message = boxes.manager().get_message(DEFAULT_USER_ID, &hash);
                if let Err(err) = message {
                    error!("{}", err);
                    continue;
                }
                let message = message.unwrap();
                if message.is_none() {
                    continue;
                }
                let message = message.unwrap();
                self.message = Some(message);

                if let Some(entry) = self
                    .entries
                    .iter_mut()
                    .find(|item| item.entry.hash() == &hash)
                {
                    if !entry.entry.read() {
                        entry.entry.set_read(true);
                        if let Err(err) = boxes.manager().set_read(DEFAULT_USER_ID, &hash, true) {
                            error!("{}", err);
                        }
                        boxes.decrement_unread_count();
                    }
                }

                self.selected = Some(hash);
            }
        }

        if let Some(s) = scrollbar {
            s.set(ui)
        }

        let label = "Unread";
        let width = text::line::width(label, ui.fonts.get(font_id).unwrap(), text_size as u32);
        for _click in widget::Button::new()
            .top_left_with_margins_on(ids.messages_buttons_canvas, 2.0, 2.0)
            .w_h(width + 8.0, text_size as f64 + 8.0)
            .label_font_size(text_size as u32)
            .label(label)
            .color(color::DARK_GRAY)
            .label_color(color::WHITE)
            .set(ids.messages_button_unread, ui)
        {
            let hash = self.selected.clone();
            if hash.is_none() {
                continue;
            }
            let hash = hash.unwrap();

            if let Some(entry) = self
                .entries
                .iter_mut()
                .find(|item| item.entry.hash() == &hash)
            {
                if entry.entry.read() {
                    entry.entry.set_read(false);
                    if let Err(err) = boxes.manager().set_read(DEFAULT_USER_ID, &hash, false) {
                        error!("{}", err);
                    }
                    boxes.increment_unread_count();
                }
            }
        }

        let label = "Copy";
        let width = text::line::width(label, ui.fonts.get(font_id).unwrap(), text_size as u32);
        for _click in widget::Button::new()
            .right_from(ids.messages_button_unread, 2.0)
            .w_h(width + 8.0, text_size as f64 + 8.0)
            .label_font_size(text_size as u32)
            .label(label)
            .color(color::DARK_GRAY)
            .label_color(color::WHITE)
            .set(ids.messages_button_copy, ui)
        {
            if self.message.is_none() {
                continue;
            }
            let message = self.message.as_ref().unwrap();

            let from = boxes.user().rich_alias(&message.from_address().to_string());
            let to = if let Some(address) = message.to_address() {
                boxes.user().rich_alias(&address.to_string())
            } else {
                String::new()
            };
            let received = time_to_string(message.time());

            let mut subject = String::new();
            let mut body = String::new();
            if message.encoding() == Encoding::Simple {
                if let Ok(simple) = Simple::try_from(message.content()) {
                    subject = String::from_utf8_lossy(simple.subject()).to_string();
                    body = String::from_utf8_lossy(simple.body()).to_string();
                }
            }
            // TODO show error when failed to parse

            let content = format!(
                "Subject: {}\nFrom: {}\nTo: {}\nReceived: {}\n\n{}",
                subject, from, to, received, body
            );

            let ctx = copypasta::ClipboardContext::new();
            if let Err(err) = ctx {
                error!("{}", err);
                continue;
            }
            let mut ctx = ctx.unwrap();
            if let Err(err) = ctx.set_contents(content) {
                error!("{}", err);
            }
        }

        let mut subject = String::new();
        let mut from = String::new();
        let mut to = String::new();
        let mut received = String::new();
        let mut body = String::new();
        if let Some(message) = &self.message {
            from = boxes.user().rich_alias(&message.from_address().to_string());
            to = if let Some(address) = message.to_address() {
                boxes.user().rich_alias(&address.to_string())
            } else {
                String::new()
            };
            received = time_to_string(message.time());

            if message.encoding() == Encoding::Simple {
                if let Ok(simple) = Simple::try_from(message.content()) {
                    subject = String::from_utf8_lossy(simple.subject()).to_string();
                    body = String::from_utf8_lossy(simple.body()).to_string();
                }
            }
        }
        // TODO show error when failed to parse

        let header = format!(
            "Subject: {}\nFrom: {}\nTo: {}\nReceived: {}",
            subject, from, to, received
        );
        widget::TextEdit::new(&header)
            .top_left_with_margins_on(
                ids.messages_header_canvas,
                text_size as f64 / 4.0 + 2.0,
                2.0,
            )
            .padded_w_of(ids.messages_header_canvas, 2.0)
            .parent(ids.messages_header_canvas)
            .restrict_to_height(false)
            .font_size(text_size as u32)
            .line_spacing(text_size as f64 / 2.0)
            .wrap_by_character()
            .set(ids.messages_header, ui);

        widget::Scrollbar::y_axis(ids.messages_header_canvas)
            .auto_hide(true)
            .set(ids.messages_header_scrollbar, ui);

        widget::TextEdit::new(&body)
            .top_left_with_margins_on(ids.messages_body_canvas, text_size as f64 / 4.0 + 2.0, 2.0)
            .padded_w_of(ids.messages_body_canvas, 2.0)
            .parent(ids.messages_body_canvas)
            .restrict_to_height(false)
            .font_size(text_size as u32)
            .line_spacing(text_size as f64 / 2.0)
            .wrap_by_character()
            .set(ids.messages_body, ui);

        widget::Scrollbar::y_axis(ids.messages_body_canvas)
            .auto_hide(true)
            .set(ids.messages_body_scrollbar, ui);
    }
}