tuitalk 0.1.0

tuitalk chatapp client which runs in the terminal
use crate::app::{App, InputMode};
use anyhow::{Context, Result};
use chrono::{Local, TimeZone, Utc};
use ratatui::{
    Frame,
    layout::{Constraint, Layout, Position},
    style::{Color, Modifier, Style, Stylize},
    text::{Line, Span, Text},
    widgets::{Block, Paragraph, Wrap},
};
use tuitalk_shared as shared;
use shared::*;
use uuid::Uuid;

fn color_from_uuid(uuid: Uuid) -> Color {
    let bytes = uuid.as_bytes();
    let r = bytes[0].saturating_add(64);
    let g = bytes[1].saturating_add(64);
    let b = bytes[2].saturating_add(64);

    Color::Rgb(r, g, b)
}

fn format_timestamp(unixtime: u64) -> Result<Span<'static>> {
    let timestamp = Utc
        .timestamp_opt(unixtime as i64, 0)
        .single()
        .context("Invalid Timestamp")?;
    Ok(Span::raw(format!(
        "<{}> ",
        timestamp.with_timezone(&Local).format("%H:%M")
    )))
}

fn return_server_error(message: &String, code: &String) -> Result<Line<'static>> {
    let error = Span::styled(format!("Server Error"), Style::default().fg(Color::Red));
    let code = Span::raw(code.clone());
    let space = Span::raw(": ".to_string());

    let message = Span::raw(message.clone());

    let content = Line::from(vec![error, space, code, message]);
    Ok(content)
}

fn return_local_error(message: &String) -> Result<Line> {
    let error = Span::styled(format!("Local Error"), Style::default().fg(Color::Red));
    let space = Span::raw(": ".to_string());

    let message = Span::raw(message);

    let content = Line::from(vec![error, space, message]);
    Ok(content)
}

fn return_local_information(message: &String) -> Result<Line> {
    let info = Span::styled(format!("Info"), Style::default().fg(Color::Green));
    let space = Span::raw(": ".to_string());

    let message = Span::raw(message);

    let content = Line::from(vec![info, space, message]);
    Ok(content)
}

fn return_user_left(unixtime: u64, username: &String, uuid: Uuid) -> Result<Line> {
    let timestamp = format_timestamp(unixtime)?;
    let info = Span::styled(format!("Info: "), Style::default().fg(Color::Yellow));
    let username = Span::styled(username, Style::default().fg(color_from_uuid(uuid)));

    let message = Span::raw(" left the room");

    let content = Line::from(vec![timestamp, info, username, message]);
    Ok(content)
}

fn return_user_joined(unixtime: u64, username: &String, uuid: Uuid) -> Result<Line> {
    let timestamp = format_timestamp(unixtime)?;

    let info = Span::styled(format!("Info: "), Style::default().fg(Color::Yellow));
    let username = Span::styled(username, Style::default().fg(color_from_uuid(uuid)));

    let message = Span::raw(" joined the room");

    let content = Line::from(vec![timestamp, info, username, message]);
    Ok(content)
}

fn return_username_changed<'a>(
    unixtime: u64,
    username: &String,
    old_username: &String,
    uuid: Uuid,
) -> Result<Line<'static>> {
    let timestamp = format_timestamp(unixtime)?;

    let info = Span::styled("Info: ".to_string(), Style::default().fg(Color::Yellow));
    let old_username = Span::styled(old_username.clone(), Style::default().fg(color_from_uuid(uuid)));

    let message = Span::raw(" changed his name to ");
    let username = Span::styled(username.clone(), Style::default().fg(color_from_uuid(uuid)));

    let content = Line::from(vec![timestamp, info, old_username, message, username]);
    Ok(content)
}

fn return_posted_message(message: &TalkMessage) -> Result<Line> {
    let timestamp = format_timestamp(message.unixtime)?;

    let username = Span::styled(
        format!("{}: ", message.username),
        Style::default().fg(color_from_uuid(message.uuid)),
    );

    let message = Span::raw(message.text.clone());

    let content = Line::from(vec![timestamp, username, message]);
    Ok(content)
}

pub fn draw(app: &mut App, frame: &mut Frame) {
    let vertical = Layout::vertical([
        Constraint::Length(1),
        Constraint::Length(3),
        Constraint::Min(1),
    ]);
    let [help_area, input_area, messages_area] = vertical.areas(frame.area());

    let (msg, style) = match app.input_mode {
        InputMode::Normal => (
            vec![
                "Press ".into(),
                "q".bold(),
                " to exit, ".into(),
                "i".bold(),
                " to start editing.".bold(),
            ],
            Style::default().add_modifier(Modifier::RAPID_BLINK),
        ),
        InputMode::Editing => (
            vec![
                "Press ".into(),
                "Esc".bold(),
                " to stop editing, ".into(),
                "Enter".bold(),
                " to send".into(),
            ],
            Style::default(),
        ),
    };
    let text = Text::from(Line::from(msg)).patch_style(style);
    frame.render_widget(Paragraph::new(text), help_area);

    let input = Paragraph::new(app.input.as_str())
        .style(match app.input_mode {
            InputMode::Normal => Style::default(),
            InputMode::Editing => Style::default().fg(Color::Yellow),
        })
        .block(Block::bordered().title("Input"));
    frame.render_widget(input, input_area);

    if let InputMode::Editing = app.input_mode {
        frame.set_cursor_position(Position::new(
            input_area.x + app.character_index as u16 + 1,
            input_area.y + 1,
        ));
    }

    let full_messages = app.communication.lock().expect("Vector with all messages");

    let lines: Vec<Line> = full_messages
        .iter()
        .map(|proto| match proto {
            TalkProtocol::Error { code, message } => return_server_error(message, code),
            TalkProtocol::LocalError { message } => return_local_error(message),
            TalkProtocol::LocalInformation { message } => return_local_information(message),
            TalkProtocol::PostMessage { message } => return_posted_message(message),
            TalkProtocol::UserJoined {
                uuid,
                username,
                room_id: _,
                unixtime,
            } => return_user_joined(*unixtime, username, uuid.clone()),
            TalkProtocol::UserLeft {
                uuid,
                username,
                room_id: _,
                unixtime,
            } => return_user_left(*unixtime, username, uuid.clone()),
            TalkProtocol::UsernameChanged {
                uuid,
                username,
                old_username,
                unixtime,
            } => return_username_changed(*unixtime, username, old_username, uuid.clone()),
            _ => Ok(Line::from(Span::raw(format!("{:?}", proto)))),
        })
        .collect::<Result<Vec<Line>, anyhow::Error>>().expect("lines of text");
    let paragraph = Paragraph::new(lines).wrap(Wrap { trim: true });

    let total_lines = paragraph.line_count(messages_area.width.into());
    let visible_height = messages_area.height.saturating_sub(2) as usize;
    app.max_scroll = total_lines.saturating_sub(visible_height);

    if app.auto_scroll {
        app.scroll = total_lines.saturating_sub(visible_height);
    }

    app.scroll = app
        .scroll
        .clamp(0, total_lines.saturating_sub(visible_height));

    frame.render_widget(
        paragraph
            .block(Block::bordered().title(format!(" Chatting in Room {} ", app.room)))
            .scroll((app.scroll as u16, 0)),
        messages_area,
    );
}