koibumi 0.0.9

An experimental Bitmessage client
use std::convert::TryFrom;

use async_std::task;
use copypasta::ClipboardProvider;
use iced::{
    button, scrollable, Button, Color, Column, Command, Element, Length, Row, Scrollable, Space,
    Text,
};
use log::error;

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

use crate::{config::Config as GuiConfig, gui, style};

#[derive(Clone, Debug)]
pub enum Message {
    EntryPressed(InvHash),
    UnreadPressed(Option<InvHash>),
    CopyPressed,
}

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

impl Entry {
    pub(crate) fn new(entry: koibumi_box::MessageEntry) -> Self {
        Self {
            entry,
            button: button::State::new(),
        }
    }
}

#[derive(Clone, Debug, Default)]
pub(crate) struct Tab {
    pub(crate) entries: Vec<Entry>,
    list_scroll: scrollable::State,
    content_scroll: scrollable::State,
    selected: Option<InvHash>,
    message: Option<koibumi_box::Message>,
    unread_button: button::State,
    copy_button: button::State,
}

fn time_to_string(time: Time) -> String {
    if time.as_secs() > i64::MAX as u64 {
        return "(unsupported)".to_string();
    }
    let mut time = ::time::OffsetDateTime::from_unix_timestamp(time.as_secs() as i64);
    if let Ok(local_offset) = ::time::UtcOffset::try_local_offset_at(time) {
        time = time.to_offset(local_offset);
    }
    time.format("%F %T %z")
}

impl Tab {
    pub(crate) fn update(
        &mut self,
        message: Message,
        boxes: &mut Option<Boxes>,
    ) -> Command<gui::Message> {
        match message {
            Message::EntryPressed(hash) => {
                if boxes.is_none() {
                    return Command::none();
                }
                let boxes = boxes.as_mut().unwrap();

                let message = task::block_on(async {
                    boxes.manager().get_message(DEFAULT_USER_ID, &hash).await
                });
                if let Err(err) = message {
                    error!("{}", err);
                    return Command::none();
                }
                let message = message.unwrap();
                if message.is_none() {
                    return Command::none();
                }
                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) =
                            task::block_on(boxes.manager().set_read(DEFAULT_USER_ID, &hash, true))
                        {
                            error!("{}", err);
                        }
                        boxes.decrement_unread_count();
                    }
                }

                self.selected = Some(hash);

                Command::none()
            }

            Message::UnreadPressed(hash) => {
                if hash.is_none() {
                    return Command::none();
                }
                let hash = hash.unwrap();

                if boxes.is_none() {
                    return Command::none();
                }
                let boxes = boxes.as_mut().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) =
                            task::block_on(boxes.manager().set_read(DEFAULT_USER_ID, &hash, false))
                        {
                            error!("{}", err);
                        }
                        boxes.increment_unread_count();
                    }
                }

                Command::none()
            }
            Message::CopyPressed => {
                if boxes.is_none() {
                    return Command::none();
                }
                let boxes = boxes.as_ref().unwrap();

                if self.message.is_none() {
                    return Command::none();
                }
                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);
                    return Command::none();
                }
                let mut ctx = ctx.unwrap();
                if let Err(err) = ctx.set_contents(content) {
                    error!("{}", err);
                }

                Command::none()
            }
        }
    }

    pub(crate) fn view(
        &mut self,
        config: &GuiConfig,
        boxes: &Option<Boxes>,
    ) -> Element<gui::Message> {
        let text_size = config.text_size();

        if boxes.is_none() {
            return Column::new()
                .push(Text::new("inbox/outbox database error").size(text_size))
                .into();
        }
        let boxes = boxes.as_ref().unwrap();

        let entry_button = |state, label, selected, hash, read| {
            let label = Text::new(label).size(text_size);
            let style = if selected {
                style::MessageEntryButton::Selected
            } else if read {
                style::MessageEntryButton::Read
            } else {
                style::MessageEntryButton::Unread
            };
            Button::new(state, label)
                .style(style)
                .on_press(gui::Message::MessagesMessage(Message::EntryPressed(hash)))
                .padding(2)
        };

        let mut list = Scrollable::new(&mut self.list_scroll).max_height(text_size as u32 * 8);
        for entry in &mut self.entries {
            let selected =
                self.selected.is_some() && self.selected.as_ref().unwrap() == entry.entry.hash();
            let row = Row::new().push(
                entry_button(
                    &mut entry.button,
                    entry.entry.subject(),
                    selected,
                    entry.entry.hash().clone(),
                    entry.entry.read(),
                )
                .width(Length::Fill),
            );
            list = list.push(row);
        }

        let buttons = Row::new()
            .spacing(text_size / 4)
            .push(
                Button::new(&mut self.unread_button, Text::new("Unread").size(text_size)).on_press(
                    gui::Message::MessagesMessage(Message::UnreadPressed(self.selected.clone())),
                ),
            )
            .push(
                Button::new(&mut self.copy_button, Text::new("Copy").size(text_size))
                    .on_press(gui::Message::MessagesMessage(Message::CopyPressed)),
            );

        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 color = Color {
            r: 0.0,
            g: 0.0,
            b: 1.0,
            a: 1.0,
        };

        let subject = Row::new()
            .spacing(text_size / 2)
            .push(Text::new("Subject:").size(text_size).color(color))
            .push(Text::new(subject).size(text_size));

        let from = Row::new()
            .spacing(text_size / 2)
            .push(Text::new("From:").size(text_size).color(color))
            .push(Text::new(from).size(text_size));

        let to = Row::new()
            .spacing(text_size / 2)
            .push(Text::new("To:").size(text_size).color(color))
            .push(Text::new(to).size(text_size));

        let received = Row::new()
            .spacing(text_size / 2)
            .push(Text::new("Received:").size(text_size).color(color))
            .push(Text::new(received).size(text_size));

        let body = Scrollable::new(&mut self.content_scroll)
            .max_height(text_size as u32 * 24)
            .push(Text::new(body).size(text_size).width(Length::Fill));

        Column::new()
            .spacing(text_size / 4)
            .push(list)
            .push(buttons)
            .push(subject)
            .push(from)
            .push(to)
            .push(received)
            .push(Space::with_height((text_size / 4).into()))
            .push(body)
            .into()
    }
}