Rain 9 месяцев назад
Родитель
Сommit
54306a2d57
4 измененных файлов с 524 добавлено и 458 удалено
  1. 19 0
      client/assets/logo.txt
  2. 45 453
      client/src/main.rs
  3. 1 1
      client/src/persistence.rs
  4. 459 4
      client/src/ui.rs

+ 19 - 0
client/assets/logo.txt

@@ -0,0 +1,19 @@
+███████████████████████████████████████████████████████████
+███                                                     ███
+███                                                     ███
+███                              ▄             █████    ███
+███                          ▟██ █           ██████     ███
+███                      ▟█▙ █   █         ██████       ███
+███                      █ █ ██  █       ███████        ███
+███                      ██▛ █   ▛    ████████          ███
+███    ██████            █ █ ██▛     ███████            ███
+███    ███████████       █           ████████████       ███
+███      ██████████████                    ████████     ███
+███          ██████████       █████                     ███
+███            ████████     ████                        ███
+███          ███████       ██                           ███
+███        ███████                                      ███
+███      █████                                          ███
+███                                                     ███
+███                                                     ███
+███████████████████████████████████████████████████████████

+ 45 - 453
client/src/main.rs

@@ -3,24 +3,19 @@ mod message;
 mod persistence;
 mod persistence;
 mod ui;
 mod ui;
 
 
+use cursive::align::Align;
 use cursive::reexports::log;
 use cursive::reexports::log;
-use cursive::views::{
-    Button, Dialog, DummyView, EditView, LinearLayout, NamedView, Panel, ScrollView, SelectView,
-    TextArea, TextView,
-};
+use cursive::views::TextView;
 use cursive::{event, Cursive};
 use cursive::{event, Cursive};
 use cursive::{traits::*, CursiveExt as _};
 use cursive::{traits::*, CursiveExt as _};
 use message::Message;
 use message::Message;
 use std::collections::HashMap;
 use std::collections::HashMap;
 use std::io::{Read as _, Write as _};
 use std::io::{Read as _, Write as _};
 use std::thread;
 use std::thread;
+use std::time::Duration;
 use utils::serialize::Serialize;
 use utils::serialize::Serialize;
 
 
-use ui::{
-    DIALOGUE_MIN_SIZE, INPUT_BUTTON_ID, INPUT_CLEAR_BUTTON_ID, INPUT_FIELD_ID, INPUT_PANEL_ID,
-    LANGUAGE_BUTTON_ID, MESSAGE_PANEL_ID, REFRESH_BUTTON_ID, USERNAME_BUTTON_ID, USERNAME_FIELD_ID,
-    USERS_PANEL_ID, USER_PANEL_SIZE,
-};
+use ui::INPUT_FIELD_ID;
 
 
 const MAX_MESSAGE_LENGTH: usize = 512;
 const MAX_MESSAGE_LENGTH: usize = 512;
 const MAX_USERNAME_LENGTH: usize = 16;
 const MAX_USERNAME_LENGTH: usize = 16;
@@ -28,6 +23,8 @@ const DEFAULT_USERNAME_PREFIX: &str = "Myst";
 const DEFAULT_CHANNEL: &str = "Root";
 const DEFAULT_CHANNEL: &str = "Root";
 const DEFAULT_PASSWORD: &str = "null";
 const DEFAULT_PASSWORD: &str = "null";
 
 
+const LOGO: &str = include_str!("../assets/logo.txt");
+
 const SAVE_FILE: &str = "savedata.bin";
 const SAVE_FILE: &str = "savedata.bin";
 const SAVE_FILE_FUZZY: u64 = 0b0110110001101001011001110110110101100001001000000101100001000100;
 const SAVE_FILE_FUZZY: u64 = 0b0110110001101001011001110110110101100001001000000101100001000100;
 
 
@@ -45,6 +42,7 @@ pub struct Appdata {
     pub messages: HashMap<String, Message>,
     pub messages: HashMap<String, Message>,
     pub quick_close_window_count: usize,
     pub quick_close_window_count: usize,
     pub local_channels: Vec<String>,
     pub local_channels: Vec<String>,
+    pub intro_screen_shown_until: u128,
 }
 }
 
 
 impl Default for Appdata {
 impl Default for Appdata {
@@ -71,6 +69,7 @@ impl Appdata {
             current_channel: DEFAULT_CHANNEL.to_string(),
             current_channel: DEFAULT_CHANNEL.to_string(),
             local_channels: vec![DEFAULT_CHANNEL.to_string()],
             local_channels: vec![DEFAULT_CHANNEL.to_string()],
             api_password: DEFAULT_PASSWORD.to_string(),
             api_password: DEFAULT_PASSWORD.to_string(),
+            intro_screen_shown_until: 0,
         }
         }
     }
     }
 }
 }
@@ -138,460 +137,53 @@ fn main() {
         let _ = siv.focus_name(INPUT_FIELD_ID);
         let _ = siv.focus_name(INPUT_FIELD_ID);
     });
     });
 
 
+    // Logo intro screen
+    siv.add_fullscreen_layer(TextView::new(LOGO).align(Align::center()).full_screen());
+
     // Background thread
     // Background thread
     {
     {
         let cb_sink = siv.cb_sink().clone();
         let cb_sink = siv.cb_sink().clone();
-        let mut timer = 0;
-        thread::spawn(move || loop {
-            cb_sink
-                .send(Box::new(move |siv| {
-                    let appdata = get_appdata(siv);
-                    ui::visual_update(siv);
-
-                    if timer % appdata.api_refresh_rate == 0 {
-                        // TODO (low): Add a notice when automatic refresh fails.
-                        let _ = actions::load_messages(siv);
-                    }
-
-                    save_appdata(siv);
-                }))
-                .expect("Failed to send callback from background thread.");
-
-            timer += 1;
-            thread::sleep(std::time::Duration::from_secs(1));
-        });
-    }
-
-    // Channels
-    let channels_panel = {
-        let mut view: SelectView<String> = SelectView::new();
-        view.set_on_submit(move |siv, channel_id: &str| {
-            siv.with_user_data(|appdata: &mut Appdata| {
-                appdata.current_channel = channel_id.to_string();
-            });
-
-            ui::visual_update(siv);
-        });
-        Panel::new(ScrollView::new(view)).with_name(ui::CHANNEL_VIEW_ID)
-    };
 
 
-    let channel_current_panel = {
-        let view = TextView::new("");
-        Panel::new(view).with_name(ui::CHANNEL_CURRENT_PANEL_ID)
-    };
-
-    let channels_new_button = Button::new("", move |siv| {
-        ui::keybind_setup_close_once(siv);
-
-        let language = get_appdata(siv).language;
-
-        siv.add_layer(
-            cursive::views::Dialog::new()
-                .title(ui::Labels::NewChannel.localize(language))
-                .content(
-                    LinearLayout::vertical().child(
-                        EditView::new()
-                            .with_name(ui::CHANNEL_NEW_FIELD_ID)
-                            .fixed_width(DIALOGUE_MIN_SIZE.0.into()),
-                    ),
-                )
-                .button(ui::Labels::Ok.localize(language), move |siv| {
-                    let input = siv
-                        .call_on_name(ui::CHANNEL_NEW_FIELD_ID, |view: &mut EditView| {
-                            view.get_content()
-                        })
-                        .expect("Failed to retrieve channel creation input.");
-
-                    if message::is_valid_username(&input) {
-                        // Valid
-                        siv.with_user_data(|appdata: &mut Appdata| {
-                            appdata.local_channels.push(input.to_string());
-                        });
+        thread::spawn(move || {
+            let mut timer = 0;
+
+            loop {
+                cb_sink
+                    .send(Box::new(move |siv| {
+                        let appdata = get_appdata(siv);
+
+                        if appdata.intro_screen_shown_until > utils::time::time_millis() {
+                            thread::sleep(Duration::from_millis(
+                                appdata
+                                    .intro_screen_shown_until
+                                    .saturating_sub(utils::time::time_millis())
+                                    as u64,
+                            ));
+                            // Main layout
+                            let main_layout = ui::setup_ui(siv);
+                            siv.pop_layer();
+                            siv.add_fullscreen_layer(main_layout);
+                        }
 
 
-                        save_appdata(siv);
                         ui::visual_update(siv);
                         ui::visual_update(siv);
-                        ui::keybind_close_manual_end(siv, false);
-                    } else {
-                        // Invalid
-                        ui::keybind_close_manual_end(siv, false); // NOTE: Error dialogue overwrites this one, so end it here beforehand.
-                        ui::error(
-                            siv,
-                            ui::Labels::InvalidChannelNameExplination.localize(language),
-                        );
-                    }
-                })
-                .button(ui::Labels::Cancel.localize(language), |siv| {
-                    ui::keybind_close_manual_end(siv, false);
-                })
-                .min_size(DIALOGUE_MIN_SIZE),
-        );
-    })
-    .with_name(ui::CHANNEL_NEW_BUTTON_ID);
-
-    // Users
-    let users_panel = {
-        let mut users_view: SelectView<String> = SelectView::new();
-        users_view.set_on_submit(move |siv, user: &String| actions::on_user_click(siv, user));
-        Panel::new(ScrollView::new(users_view)).with_name(USERS_PANEL_ID)
-    };
-
-    // Messages
-    let messages_panel = {
-        let mut view: SelectView<String> = SelectView::new();
-        view.set_on_submit(move |siv, message_id: &str| actions::on_message_click(siv, message_id));
-        Panel::new(ScrollView::new(view)).with_name(MESSAGE_PANEL_ID)
-    };
 
 
-    // Input
-    let input_panel = {
-        let input_field = TextArea::new()
-            .with_name(INPUT_FIELD_ID)
-            .full_width()
-            .max_height(10); // NOTE: 10 seems to be the size of 2 vertical buttons
-        let input_button =
-            Button::new("", move |siv| actions::on_input_submit(siv, INPUT_FIELD_ID))
-                .with_name(INPUT_BUTTON_ID);
-
-        let input_clear_button =
-            Button::new("", actions::clear_input_field).with_name(INPUT_CLEAR_BUTTON_ID);
-
-        Panel::new(
-            LinearLayout::horizontal().child(input_field).child(
-                LinearLayout::vertical()
-                    .child(input_button)
-                    .child(input_clear_button),
-            ),
-        )
-        .with_name(INPUT_PANEL_ID)
-    };
-
-    // Language selector
-    let language_button = Button::new("", move |siv| {
-        // NOTE: This must match the order in the SelectView
-        let current_language_index = match get_appdata(siv).language {
-            ui::Language::English => 0,
-            ui::Language::Dutch => 1,
-            ui::Language::Japanese => 2,
-        };
-
-        let language = get_appdata(siv).language;
-
-        ui::keybind_setup_close_once(siv);
-
-        siv.add_layer(
-            cursive::views::Dialog::new()
-                .content(
-                    LinearLayout::vertical().child(
-                        SelectView::<ui::Language>::new()
-                            .item("English", ui::Language::English)
-                            .item("Nederlands", ui::Language::Dutch)
-                            .item("日本語", ui::Language::Japanese)
-                            .selected(current_language_index)
-                            .on_submit(move |siv, language| {
-                                ui::change_language(siv, *language);
-                                ui::keybind_close_manual_end(siv, false);
-                            }),
-                    ),
-                )
-                .title(ui::Labels::SetLanguage.localize(language))
-                .button(ui::Labels::Cancel.localize(language), |siv| {
-                    ui::keybind_close_manual_end(siv, false);
-                })
-                .min_size(DIALOGUE_MIN_SIZE),
-        );
-    })
-    .with_name(LANGUAGE_BUTTON_ID);
-
-    // Current username panel
-    let current_username_panel = {
-        let current_username = get_appdata(&mut siv).username.clone();
-        let view = TextView::new(current_username);
-        Panel::new(view).with_name(ui::CURRENT_USERNAME_PANEL_ID)
-    };
-
-    // Set username
-    let username_button = Button::new("", move |siv| {
-        let current_name = get_appdata(siv).username.clone();
-        let language = get_appdata(siv).language;
-
-        ui::keybind_setup_close_once(siv);
-
-        siv.add_layer(
-            cursive::views::Dialog::new()
-                .title(ui::Labels::Username.localize(language))
-                .content(
-                    LinearLayout::vertical().child(
-                        EditView::new()
-                            .content(current_name)
-                            .with_name(USERNAME_FIELD_ID)
-                            .fixed_width(DIALOGUE_MIN_SIZE.0.into()),
-                    ),
-                )
-                .button(ui::Labels::Ok.localize(language), |siv| {
-                    let input = siv
-                        .call_on_name(USERNAME_FIELD_ID, |view: &mut EditView| view.get_content())
-                        .expect("Failed to retrieve username input.");
-
-                    if message::is_valid_username(&input) {
-                        // Valid
-                        siv.with_user_data(|appdata: &mut Appdata| {
-                            appdata.username = input.to_string();
-                        });
+                        if timer % appdata.api_refresh_rate == 0 {
+                            // TODO (low): Add a notice when automatic refresh fails.
+                            let _ = actions::load_messages(siv);
+                        }
 
 
                         save_appdata(siv);
                         save_appdata(siv);
-                        ui::visual_update(siv);
-                        ui::keybind_close_manual_end(siv, false);
-                    } else {
-                        // Invalid
-                        let language = get_appdata(siv).language;
-                        ui::keybind_close_manual_end(siv, false); // NOTE: Error dialogue overwrites this one, so end it here beforehand.
-                        ui::error(
-                            siv,
-                            ui::Labels::InvalidUsernameExplination.localize(language),
-                        );
-                    }
-                })
-                .button(ui::Labels::Cancel.localize(language), |siv| {
-                    ui::keybind_close_manual_end(siv, false);
-                })
-                .min_size(DIALOGUE_MIN_SIZE),
-        );
-    })
-    .with_name(USERNAME_BUTTON_ID);
-
-    // Refresh button
-    let refresh_button = Button::new("", move |siv| {
-        if let Err(e) = actions::load_messages(siv) {
-            let language = get_appdata(siv).language;
-            ui::error(siv, ui::Labels::FailedToLoadMessages(e).localize(language));
-        }
-        ui::visual_update(siv);
-    })
-    .with_name(REFRESH_BUTTON_ID);
-
-    // Blocked words list
-    let blocked_words_list_button = {
-        Button::new("", move |siv| {
-            let appdata = get_appdata(siv);
-            let language = appdata.language;
-
-            let wordslist_id = utils::new_id();
-            let wordslist_id_clone = wordslist_id.clone();
-            let wordslist_id_typingarea_clone = wordslist_id.clone();
-            let mut wordslist_view =
-                SelectView::<String>::new().on_submit(move |siv, word: &str| {
-                    siv.with_user_data(|appdata: &mut Appdata| {
-                        appdata.blocked_phrases.retain(|x| x != word);
-                    });
-
-                    siv.call_on_name(&wordslist_id_clone, |view: &mut NamedView<SelectView>| {
-                        let idx = view
-                            .get_mut()
-                            .selected_id()
-                            .expect("Selection should exist when fired from submit.");
-                        view.get_mut().remove_item(idx);
-                    });
-
-                    save_appdata(siv);
-                    ui::visual_update(siv);
-                });
-            wordslist_view.add_all_str(&appdata.blocked_phrases);
-
-            let typingarea_id = utils::new_id();
-            let typing_area = EditView::new().with_name(&typingarea_id);
-            let typing_button = Button::new(ui::Labels::Submit.localize(language), move |siv| {
-                let text = siv
-                    .call_on_name(&typingarea_id, |view: &mut EditView| {
-                        let s = utils::strings::insensitive_string(&view.get_content());
-                        view.set_content("");
-                        s
-                    })
-                    .map(|x| x.to_string())
-                    .unwrap_or("".to_string());
-
-                if get_appdata(siv).blocked_phrases.contains(&text) || text.is_empty() {
-                    return;
-                }
-
-                siv.with_user_data(|appdata: &mut Appdata| {
-                    appdata.blocked_phrases.push(text.clone());
-                });
-
-                siv.call_on_name(
-                    &wordslist_id_typingarea_clone,
-                    |view: &mut NamedView<SelectView>| {
-                        view.get_mut().add_item_str(text);
-                    },
-                );
-
-                save_appdata(siv);
-                ui::visual_update(siv);
-            });
-
-            let d = Dialog::new()
-                .title(ui::Labels::BlockedWords.localize(language))
-                .button(ui::Labels::Close.localize(language), |siv| {
-                    ui::keybind_close_manual_end(siv, false);
-                })
-                .content(
-                    LinearLayout::vertical()
-                        .child(ScrollView::new(wordslist_view.with_name(wordslist_id)))
-                        .child(DummyView.fixed_height(1))
-                        .child(
-                            LinearLayout::horizontal()
-                                .child(typing_area.min_width(DIALOGUE_MIN_SIZE.0 as usize))
-                                .child(typing_button),
-                        ),
-                );
-
-            ui::keybind_setup_close_once(siv);
-            siv.add_layer(d);
-        })
-        .with_name(ui::BLOCKED_WORDS_BUTTON_ID)
-    };
+                    }))
+                    .expect("Failed to send callback from background thread.");
 
 
-    // Server settings
-    let server_settings_button = Button::new("", |siv| {
-        let appdata = get_appdata(siv);
-        let language = appdata.language;
-
-        ui::keybind_setup_close_once(siv);
-
-        siv.add_layer(
-            cursive::views::Dialog::new()
-                .title(ui::Labels::ServerSettings.localize(language))
-                .content(
-                    LinearLayout::vertical()
-                        .child(
-                            LinearLayout::horizontal()
-                                .child(
-                                    TextView::new(ui::Labels::ServerAddress.localize(language))
-                                        .fixed_width(ui::SERVER_SETTINGS_FIELD_SIZE.0 / 2),
-                                )
-                                .child(DummyView.fixed_width(1))
-                                .child(
-                                    EditView::new()
-                                        .content(appdata.api_endpoint)
-                                        .with_name(ui::SERVER_SETTINGS_ADDRESS_FIELD_ID)
-                                        .min_width(ui::SERVER_SETTINGS_FIELD_SIZE.0)
-                                        .max_height(ui::SERVER_SETTINGS_FIELD_SIZE.1),
-                                ),
-                        )
-                        .child(
-                            LinearLayout::horizontal()
-                                .child(
-                                    TextView::new(ui::Labels::ServerRefreshRate.localize(language))
-                                        .fixed_width(ui::SERVER_SETTINGS_FIELD_SIZE.0 / 2),
-                                )
-                                .child(DummyView.fixed_width(1))
-                                .child(
-                                    EditView::new()
-                                        .content(appdata.api_refresh_rate.to_string())
-                                        .with_name(ui::SERVER_SETTINGS_REFRESH_FIELD_ID)
-                                        .min_width(ui::SERVER_SETTINGS_FIELD_SIZE.0)
-                                        .max_height(ui::SERVER_SETTINGS_FIELD_SIZE.1),
-                                ),
-                        )
-                        .child(
-                            LinearLayout::horizontal()
-                                .child(
-                                    TextView::new(ui::Labels::Password.localize(language))
-                                        .fixed_width(ui::SERVER_SETTINGS_FIELD_SIZE.0 / 2),
-                                )
-                                .child(DummyView.fixed_width(1))
-                                .child(
-                                    EditView::new()
-                                        .content(appdata.api_password.to_string())
-                                        .with_name(ui::SERVER_SETTINGS_PASSWORD_FIELD_ID)
-                                        .min_width(ui::SERVER_SETTINGS_FIELD_SIZE.0)
-                                        .max_height(ui::SERVER_SETTINGS_FIELD_SIZE.1),
-                                ),
-                        ),
-                )
-                .button(ui::Labels::Ok.localize(language), |siv| {
-                    let input_addr = siv
-                        .call_on_name(
-                            ui::SERVER_SETTINGS_ADDRESS_FIELD_ID,
-                            |view: &mut EditView| view.get_content(),
-                        )
-                        .expect("Failed to retrieve server settings address input.");
-
-                    let input_refresh = siv
-                        .call_on_name(
-                            ui::SERVER_SETTINGS_REFRESH_FIELD_ID,
-                            |view: &mut EditView| view.get_content(),
-                        )
-                        .expect("Failed to retrieve server settings refresh rate input.");
-
-                    let input_password = siv
-                        .call_on_name(
-                            ui::SERVER_SETTINGS_PASSWORD_FIELD_ID,
-                            |view: &mut EditView| view.get_content(),
-                        )
-                        .expect("Failed to retrieve server settings password input.");
-
-                    siv.with_user_data(|appdata: &mut Appdata| {
-                        appdata.api_endpoint = input_addr.to_string();
-
-                        let mut password = input_password.to_string();
-                        if password.is_empty() {
-                            password = DEFAULT_PASSWORD.to_string();
-                        }
-                        appdata.api_password = password;
+                timer += 1;
+                thread::sleep(Duration::from_secs(1));
+            }
+        });
+    }
 
 
-                        if let Ok(rate) = input_refresh.parse() {
-                            appdata.api_refresh_rate = rate;
-                        }
-                    });
-
-                    save_appdata(siv);
-                    ui::visual_update(siv);
-                    ui::keybind_close_manual_end(siv, false);
-                })
-                .button(ui::Labels::Cancel.localize(language), |siv| {
-                    ui::keybind_close_manual_end(siv, false);
-                })
-                .min_size(DIALOGUE_MIN_SIZE),
-        );
-    })
-    .with_name(ui::SERVER_SETTINGS_BUTTON_ID);
-
-    // Main layout
-    let main_layout = LinearLayout::vertical()
-        .child(
-            LinearLayout::horizontal()
-                .child(
-                    LinearLayout::vertical()
-                        .child(channels_panel.fixed_width(USER_PANEL_SIZE).full_height())
-                        .child(channel_current_panel),
-                )
-                .child(
-                    LinearLayout::vertical()
-                        .child(users_panel.resized(
-                            cursive::view::SizeConstraint::Fixed(USER_PANEL_SIZE),
-                            cursive::view::SizeConstraint::Full,
-                        ))
-                        .child(current_username_panel),
-                )
-                .child(messages_panel.full_width()),
-        )
-        .child(input_panel)
-        .child(
-            LinearLayout::horizontal()
-                .child(language_button)
-                .child(DummyView.full_width())
-                .child(username_button)
-                .child(DummyView.full_width())
-                .child(channels_new_button)
-                .child(DummyView.full_width())
-                .child(blocked_words_list_button)
-                .child(DummyView.full_width())
-                .child(server_settings_button)
-                .child(DummyView.full_width())
-                .child(refresh_button)
-                .full_width(),
-        );
-
-    siv.add_fullscreen_layer(main_layout);
+    siv.with_user_data(|appdata: &mut Appdata| {
+        appdata.intro_screen_shown_until = utils::time::time_millis() + 2000;
+    });
     siv.run();
     siv.run();
 }
 }

+ 1 - 1
client/src/persistence.rs

@@ -38,7 +38,7 @@ impl From<Savedata> for Appdata {
         s.blocked_phrases_censor_char = value.blocked_phrases_censor_char;
         s.blocked_phrases_censor_char = value.blocked_phrases_censor_char;
         s.api_endpoint = value.api_endpoint;
         s.api_endpoint = value.api_endpoint;
         s.api_refresh_rate = value.api_refresh_rate as usize;
         s.api_refresh_rate = value.api_refresh_rate as usize;
-        s.current_channel = value.current_channel;
+        // s.current_channel = value.current_channel;
         s.api_password = value.api_password;
         s.api_password = value.api_password;
 
 
         s
         s

+ 459 - 4
client/src/ui.rs

@@ -1,13 +1,17 @@
-use cursive::view::Resizable as _;
+use cursive::view::{Nameable as _, Resizable as _};
 use cursive::views::{
 use cursive::views::{
-    Button, Dialog, LinearLayout, NamedView, Panel, ScrollView, SelectView, TextView,
+    Button, Dialog, DummyView, EditView, LinearLayout, NamedView, Panel, ScrollView, SelectView,
+    TextArea, TextView,
 };
 };
 use cursive::{event, Cursive};
 use cursive::{event, Cursive};
 use utils::hash::Hashable as _;
 use utils::hash::Hashable as _;
 
 
-use crate::actions::NetworkError;
+use crate::actions::{self, NetworkError};
 use crate::message::MessageSanitized;
 use crate::message::MessageSanitized;
-use crate::{get_appdata, message, Appdata, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH};
+use crate::{
+    get_appdata, message, save_appdata, Appdata, DEFAULT_PASSWORD, MAX_MESSAGE_LENGTH,
+    MAX_USERNAME_LENGTH,
+};
 
 
 pub const USERS_PANEL_ID: &str = "users_view_id";
 pub const USERS_PANEL_ID: &str = "users_view_id";
 pub const MESSAGE_PANEL_ID: &str = "message_view_id";
 pub const MESSAGE_PANEL_ID: &str = "message_view_id";
@@ -29,6 +33,7 @@ pub const SERVER_SETTINGS_ADDRESS_FIELD_ID: &str = "server_settings_address_fiel
 pub const SERVER_SETTINGS_REFRESH_FIELD_ID: &str = "server_settings_refresh_field_id";
 pub const SERVER_SETTINGS_REFRESH_FIELD_ID: &str = "server_settings_refresh_field_id";
 pub const SERVER_SETTINGS_PASSWORD_FIELD_ID: &str = "SERVER_SETTINGS_PASSWORD_FIELD_ID";
 pub const SERVER_SETTINGS_PASSWORD_FIELD_ID: &str = "SERVER_SETTINGS_PASSWORD_FIELD_ID";
 pub const SERVER_SETTINGS_BUTTON_ID: &str = "server_settings_button_id";
 pub const SERVER_SETTINGS_BUTTON_ID: &str = "server_settings_button_id";
+pub const INFO_BUTTON_ID: &str = "INFO_BUTTON_ID";
 
 
 pub const DIALOGUE_MIN_SIZE: (u16, u16) = (20, 5);
 pub const DIALOGUE_MIN_SIZE: (u16, u16) = (20, 5);
 pub const SERVER_SETTINGS_FIELD_SIZE: (usize, usize) = (60, 1);
 pub const SERVER_SETTINGS_FIELD_SIZE: (usize, usize) = (60, 1);
@@ -68,6 +73,7 @@ pub enum Labels {
     ServerAddress,
     ServerAddress,
     ServerRefreshRate,
     ServerRefreshRate,
     Password,
     Password,
+    Information,
 }
 }
 
 
 #[derive(Debug, Clone, Copy)]
 #[derive(Debug, Clone, Copy)]
@@ -263,6 +269,11 @@ impl Labels {
                 "Bericht verzonden!",
                 "Bericht verzonden!",
                 "メッセージを送信しました!",
                 "メッセージを送信しました!",
             ],
             ],
+            Self::Information => [
+                "Info",
+                "Informatie",
+                "情報",
+            ],
         };
         };
 
 
         let idx = match language {
         let idx = match language {
@@ -512,6 +523,11 @@ pub fn visual_update(siv: &mut Cursive) {
             .set_label(Labels::BlockedWords.localize(language));
             .set_label(Labels::BlockedWords.localize(language));
     });
     });
 
 
+    siv.call_on_name(INFO_BUTTON_ID, |view: &mut NamedView<Button>| {
+        view.get_mut()
+            .set_label(Labels::Information.localize(language));
+    });
+
     siv.call_on_name(
     siv.call_on_name(
         CURRENT_USERNAME_PANEL_ID,
         CURRENT_USERNAME_PANEL_ID,
         |view: &mut NamedView<Panel<TextView>>| {
         |view: &mut NamedView<Panel<TextView>>| {
@@ -538,3 +554,442 @@ pub fn change_language(siv: &mut Cursive, language: Language) {
 pub fn channel_matches(message_channel: &str, reference_channel: &str) -> bool {
 pub fn channel_matches(message_channel: &str, reference_channel: &str) -> bool {
     message_channel == reference_channel
     message_channel == reference_channel
 }
 }
+
+pub fn setup_ui(siv: &mut Cursive) -> LinearLayout {
+    // Channels
+    let channels_panel = {
+        let mut view: SelectView<String> = SelectView::new();
+        view.set_on_submit(move |siv, channel_id: &str| {
+            siv.with_user_data(|appdata: &mut Appdata| {
+                appdata.current_channel = channel_id.to_string();
+            });
+
+            visual_update(siv);
+        });
+        Panel::new(ScrollView::new(view)).with_name(CHANNEL_VIEW_ID)
+    };
+
+    let channel_current_panel = {
+        let view = TextView::new("");
+        Panel::new(view).with_name(CHANNEL_CURRENT_PANEL_ID)
+    };
+
+    let channels_new_button = Button::new("", move |siv| {
+        keybind_setup_close_once(siv);
+
+        let language = get_appdata(siv).language;
+
+        siv.add_layer(
+            cursive::views::Dialog::new()
+                .title(Labels::NewChannel.localize(language))
+                .content(
+                    LinearLayout::vertical().child(
+                        EditView::new()
+                            .with_name(CHANNEL_NEW_FIELD_ID)
+                            .fixed_width(DIALOGUE_MIN_SIZE.0.into()),
+                    ),
+                )
+                .button(Labels::Ok.localize(language), move |siv| {
+                    let input = siv
+                        .call_on_name(CHANNEL_NEW_FIELD_ID, |view: &mut EditView| {
+                            view.get_content()
+                        })
+                        .expect("Failed to retrieve channel creation input.");
+
+                    if message::is_valid_username(&input) {
+                        // Valid
+                        siv.with_user_data(|appdata: &mut Appdata| {
+                            appdata.local_channels.push(input.to_string());
+                            appdata.current_channel = input.to_string();
+                        });
+
+                        save_appdata(siv);
+                        visual_update(siv);
+                        keybind_close_manual_end(siv, false);
+                    } else {
+                        // Invalid
+                        keybind_close_manual_end(siv, false); // NOTE: Error dialogue overwrites this one, so end it here beforehand.
+                        error(
+                            siv,
+                            Labels::InvalidChannelNameExplination.localize(language),
+                        );
+                    }
+                })
+                .button(Labels::Cancel.localize(language), |siv| {
+                    keybind_close_manual_end(siv, false);
+                })
+                .min_size(DIALOGUE_MIN_SIZE),
+        );
+    })
+    .with_name(CHANNEL_NEW_BUTTON_ID);
+
+    // Users
+    let users_panel = {
+        let mut users_view: SelectView<String> = SelectView::new();
+        users_view.set_on_submit(move |siv, user: &String| actions::on_user_click(siv, user));
+        Panel::new(ScrollView::new(users_view)).with_name(USERS_PANEL_ID)
+    };
+
+    // Messages
+    let messages_panel = {
+        let mut view: SelectView<String> = SelectView::new();
+        view.set_on_submit(move |siv, message_id: &str| actions::on_message_click(siv, message_id));
+        Panel::new(ScrollView::new(view)).with_name(MESSAGE_PANEL_ID)
+    };
+
+    // Input
+    let input_panel = {
+        let input_field = TextArea::new()
+            .with_name(INPUT_FIELD_ID)
+            .full_width()
+            .max_height(10); // NOTE: 10 seems to be the size of 2 vertical buttons
+        let input_button =
+            Button::new("", move |siv| actions::on_input_submit(siv, INPUT_FIELD_ID))
+                .with_name(INPUT_BUTTON_ID);
+
+        let input_clear_button =
+            Button::new("", actions::clear_input_field).with_name(INPUT_CLEAR_BUTTON_ID);
+
+        Panel::new(
+            LinearLayout::horizontal().child(input_field).child(
+                LinearLayout::vertical()
+                    .child(input_button)
+                    .child(input_clear_button),
+            ),
+        )
+        .with_name(INPUT_PANEL_ID)
+    };
+
+    // Language selector
+    let language_button = Button::new("", move |siv| {
+        // NOTE: This must match the order in the SelectView
+        let current_language_index = match get_appdata(siv).language {
+            Language::English => 0,
+            Language::Dutch => 1,
+            Language::Japanese => 2,
+        };
+
+        let language = get_appdata(siv).language;
+
+        keybind_setup_close_once(siv);
+
+        siv.add_layer(
+            cursive::views::Dialog::new()
+                .content(
+                    LinearLayout::vertical().child(
+                        SelectView::<Language>::new()
+                            .item("English", Language::English)
+                            .item("Nederlands", Language::Dutch)
+                            .item("日本語", Language::Japanese)
+                            .selected(current_language_index)
+                            .on_submit(move |siv, language| {
+                                change_language(siv, *language);
+                                keybind_close_manual_end(siv, false);
+                            }),
+                    ),
+                )
+                .title(Labels::SetLanguage.localize(language))
+                .button(Labels::Cancel.localize(language), |siv| {
+                    keybind_close_manual_end(siv, false);
+                })
+                .min_size(DIALOGUE_MIN_SIZE),
+        );
+    })
+    .with_name(LANGUAGE_BUTTON_ID);
+
+    // Current username panel
+    let current_username_panel = {
+        let current_username = get_appdata(siv).username.clone();
+        let view = TextView::new(current_username);
+        Panel::new(view).with_name(CURRENT_USERNAME_PANEL_ID)
+    };
+
+    // Set username
+    let username_button = Button::new("", move |siv| {
+        let current_name = get_appdata(siv).username.clone();
+        let language = get_appdata(siv).language;
+
+        keybind_setup_close_once(siv);
+
+        siv.add_layer(
+            cursive::views::Dialog::new()
+                .title(Labels::Username.localize(language))
+                .content(
+                    LinearLayout::vertical().child(
+                        EditView::new()
+                            .content(current_name)
+                            .with_name(USERNAME_FIELD_ID)
+                            .fixed_width(DIALOGUE_MIN_SIZE.0.into()),
+                    ),
+                )
+                .button(Labels::Ok.localize(language), |siv| {
+                    let input = siv
+                        .call_on_name(USERNAME_FIELD_ID, |view: &mut EditView| view.get_content())
+                        .expect("Failed to retrieve username input.");
+
+                    if message::is_valid_username(&input) {
+                        // Valid
+                        siv.with_user_data(|appdata: &mut Appdata| {
+                            appdata.username = input.to_string();
+                        });
+
+                        save_appdata(siv);
+                        visual_update(siv);
+                        keybind_close_manual_end(siv, false);
+                    } else {
+                        // Invalid
+                        let language = get_appdata(siv).language;
+                        keybind_close_manual_end(siv, false); // NOTE: Error dialogue overwrites this one, so end it here beforehand.
+                        error(siv, Labels::InvalidUsernameExplination.localize(language));
+                    }
+                })
+                .button(Labels::Cancel.localize(language), |siv| {
+                    keybind_close_manual_end(siv, false);
+                })
+                .min_size(DIALOGUE_MIN_SIZE),
+        );
+    })
+    .with_name(USERNAME_BUTTON_ID);
+
+    // Refresh button
+    let refresh_button = Button::new("", move |siv| {
+        if let Err(e) = actions::load_messages(siv) {
+            let language = get_appdata(siv).language;
+            error(siv, Labels::FailedToLoadMessages(e).localize(language));
+        }
+        visual_update(siv);
+    })
+    .with_name(REFRESH_BUTTON_ID);
+
+    // Blocked words list
+    let blocked_words_list_button = {
+        Button::new("", move |siv| {
+            let appdata = get_appdata(siv);
+            let language = appdata.language;
+
+            let wordslist_id = utils::new_id();
+            let wordslist_id_clone = wordslist_id.clone();
+            let wordslist_id_typingarea_clone = wordslist_id.clone();
+            let mut wordslist_view =
+                SelectView::<String>::new().on_submit(move |siv, word: &str| {
+                    siv.with_user_data(|appdata: &mut Appdata| {
+                        appdata.blocked_phrases.retain(|x| x != word);
+                    });
+
+                    siv.call_on_name(&wordslist_id_clone, |view: &mut NamedView<SelectView>| {
+                        let idx = view
+                            .get_mut()
+                            .selected_id()
+                            .expect("Selection should exist when fired from submit.");
+                        view.get_mut().remove_item(idx);
+                    });
+
+                    save_appdata(siv);
+                    visual_update(siv);
+                });
+            wordslist_view.add_all_str(&appdata.blocked_phrases);
+
+            let typingarea_id = utils::new_id();
+            let typing_area = EditView::new().with_name(&typingarea_id);
+            let typing_button = Button::new(Labels::Submit.localize(language), move |siv| {
+                let text = siv
+                    .call_on_name(&typingarea_id, |view: &mut EditView| {
+                        let s = utils::strings::insensitive_string(&view.get_content());
+                        view.set_content("");
+                        s
+                    })
+                    .map(|x| x.to_string())
+                    .unwrap_or("".to_string());
+
+                if get_appdata(siv).blocked_phrases.contains(&text) || text.is_empty() {
+                    return;
+                }
+
+                siv.with_user_data(|appdata: &mut Appdata| {
+                    appdata.blocked_phrases.push(text.clone());
+                });
+
+                siv.call_on_name(
+                    &wordslist_id_typingarea_clone,
+                    |view: &mut NamedView<SelectView>| {
+                        view.get_mut().add_item_str(text);
+                    },
+                );
+
+                save_appdata(siv);
+                visual_update(siv);
+            });
+
+            let d = Dialog::new()
+                .title(Labels::BlockedWords.localize(language))
+                .button(Labels::Close.localize(language), |siv| {
+                    keybind_close_manual_end(siv, false);
+                })
+                .content(
+                    LinearLayout::vertical()
+                        .child(ScrollView::new(wordslist_view.with_name(wordslist_id)))
+                        .child(DummyView.fixed_height(1))
+                        .child(
+                            LinearLayout::horizontal()
+                                .child(typing_area.min_width(DIALOGUE_MIN_SIZE.0 as usize))
+                                .child(typing_button),
+                        ),
+                );
+
+            keybind_setup_close_once(siv);
+            siv.add_layer(d);
+        })
+        .with_name(BLOCKED_WORDS_BUTTON_ID)
+    };
+
+    // Server settings
+    let server_settings_button = Button::new("", |siv| {
+        let appdata = get_appdata(siv);
+        let language = appdata.language;
+
+        keybind_setup_close_once(siv);
+
+        siv.add_layer(
+            cursive::views::Dialog::new()
+                .title(Labels::ServerSettings.localize(language))
+                .content(
+                    LinearLayout::vertical()
+                        .child(
+                            LinearLayout::horizontal()
+                                .child(
+                                    TextView::new(Labels::ServerAddress.localize(language))
+                                        .fixed_width(SERVER_SETTINGS_FIELD_SIZE.0 / 2),
+                                )
+                                .child(DummyView.fixed_width(1))
+                                .child(
+                                    EditView::new()
+                                        .content(appdata.api_endpoint)
+                                        .with_name(SERVER_SETTINGS_ADDRESS_FIELD_ID)
+                                        .min_width(SERVER_SETTINGS_FIELD_SIZE.0)
+                                        .max_height(SERVER_SETTINGS_FIELD_SIZE.1),
+                                ),
+                        )
+                        .child(
+                            LinearLayout::horizontal()
+                                .child(
+                                    TextView::new(Labels::ServerRefreshRate.localize(language))
+                                        .fixed_width(SERVER_SETTINGS_FIELD_SIZE.0 / 2),
+                                )
+                                .child(DummyView.fixed_width(1))
+                                .child(
+                                    EditView::new()
+                                        .content(appdata.api_refresh_rate.to_string())
+                                        .with_name(SERVER_SETTINGS_REFRESH_FIELD_ID)
+                                        .min_width(SERVER_SETTINGS_FIELD_SIZE.0)
+                                        .max_height(SERVER_SETTINGS_FIELD_SIZE.1),
+                                ),
+                        )
+                        .child(
+                            LinearLayout::horizontal()
+                                .child(
+                                    TextView::new(Labels::Password.localize(language))
+                                        .fixed_width(SERVER_SETTINGS_FIELD_SIZE.0 / 2),
+                                )
+                                .child(DummyView.fixed_width(1))
+                                .child(
+                                    EditView::new()
+                                        .content(appdata.api_password.to_string())
+                                        .with_name(SERVER_SETTINGS_PASSWORD_FIELD_ID)
+                                        .min_width(SERVER_SETTINGS_FIELD_SIZE.0)
+                                        .max_height(SERVER_SETTINGS_FIELD_SIZE.1),
+                                ),
+                        ),
+                )
+                .button(Labels::Ok.localize(language), |siv| {
+                    let input_addr = siv
+                        .call_on_name(SERVER_SETTINGS_ADDRESS_FIELD_ID, |view: &mut EditView| {
+                            view.get_content()
+                        })
+                        .expect("Failed to retrieve server settings address input.");
+
+                    let input_refresh = siv
+                        .call_on_name(SERVER_SETTINGS_REFRESH_FIELD_ID, |view: &mut EditView| {
+                            view.get_content()
+                        })
+                        .expect("Failed to retrieve server settings refresh rate input.");
+
+                    let input_password = siv
+                        .call_on_name(SERVER_SETTINGS_PASSWORD_FIELD_ID, |view: &mut EditView| {
+                            view.get_content()
+                        })
+                        .expect("Failed to retrieve server settings password input.");
+
+                    siv.with_user_data(|appdata: &mut Appdata| {
+                        appdata.api_endpoint = input_addr.to_string();
+
+                        let mut password = input_password.to_string();
+                        if password.is_empty() {
+                            password = DEFAULT_PASSWORD.to_string();
+                        }
+                        appdata.api_password = password;
+
+                        if let Ok(rate) = input_refresh.parse() {
+                            appdata.api_refresh_rate = rate;
+                        }
+                    });
+
+                    save_appdata(siv);
+                    visual_update(siv);
+                    keybind_close_manual_end(siv, false);
+                })
+                .button(Labels::Cancel.localize(language), |siv| {
+                    keybind_close_manual_end(siv, false);
+                })
+                .min_size(DIALOGUE_MIN_SIZE),
+        );
+    })
+    .with_name(SERVER_SETTINGS_BUTTON_ID);
+
+    let info_button = Button::new("", move |siv| {
+        let appdata = get_appdata(siv);
+        let language = appdata.language;
+
+        alert(siv, Labels::Information.localize(language), "Rei <rain@skuld.network>".to_string());
+    })
+    .with_name(INFO_BUTTON_ID);
+
+    // Main layout
+    let main_layout = LinearLayout::vertical()
+        .child(
+            LinearLayout::horizontal()
+                .child(
+                    LinearLayout::vertical()
+                        .child(channels_panel.fixed_width(USER_PANEL_SIZE).full_height())
+                        .child(channel_current_panel),
+                )
+                .child(
+                    LinearLayout::vertical()
+                        .child(users_panel.resized(
+                            cursive::view::SizeConstraint::Fixed(USER_PANEL_SIZE),
+                            cursive::view::SizeConstraint::Full,
+                        ))
+                        .child(current_username_panel),
+                )
+                .child(messages_panel.full_width()),
+        )
+        .child(input_panel)
+        .child(
+            LinearLayout::horizontal()
+                .child(info_button)
+                .child(DummyView.full_width())
+                .child(language_button)
+                .child(DummyView.full_width())
+                .child(username_button)
+                .child(DummyView.full_width())
+                .child(channels_new_button)
+                .child(DummyView.full_width())
+                .child(blocked_words_list_button)
+                .child(DummyView.full_width())
+                .child(server_settings_button)
+                .child(DummyView.full_width())
+                .child(refresh_button)
+                .full_width(),
+        );
+
+    main_layout
+}