ui.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. use cursive::view::Resizable as _;
  2. use cursive::views::{
  3. Button, Dialog, LinearLayout, NamedView, Panel, ScrollView, SelectView, TextView,
  4. };
  5. use cursive::{event, Cursive};
  6. use utils::hash::Hashable as _;
  7. use crate::actions::NetworkError;
  8. use crate::message::MessageSanitized;
  9. use crate::{get_appdata, message, Appdata, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH};
  10. pub const USERS_PANEL_ID: &str = "users_view_id";
  11. pub const MESSAGE_PANEL_ID: &str = "message_view_id";
  12. pub const CHANNEL_VIEW_ID: &str = "channel_view_id";
  13. pub const CHANNEL_NEW_FIELD_ID: &str = "channel_new_field_id";
  14. pub const CHANNEL_NEW_BUTTON_ID: &str = "CHANNEL_NEW_BUTTON_ID";
  15. pub const CHANNEL_CURRENT_PANEL_ID: &str = "CHANNEL_CURRENT_PANEL_ID";
  16. pub const INPUT_FIELD_ID: &str = "input_field_id";
  17. pub const INPUT_PANEL_ID: &str = "input_panel_id";
  18. pub const INPUT_BUTTON_ID: &str = "input_button_id";
  19. pub const INPUT_CLEAR_BUTTON_ID: &str = "input_clear_button_id";
  20. pub const LANGUAGE_BUTTON_ID: &str = "language_button_id";
  21. pub const CURRENT_USERNAME_PANEL_ID: &str = "current_username_view_id";
  22. pub const USERNAME_FIELD_ID: &str = "username_field_id";
  23. pub const USERNAME_BUTTON_ID: &str = "username_button_id";
  24. pub const REFRESH_BUTTON_ID: &str = "refresh_button_id";
  25. pub const BLOCKED_WORDS_BUTTON_ID: &str = "blocked_words_view_id";
  26. pub const SERVER_SETTINGS_ADDRESS_FIELD_ID: &str = "server_settings_address_field_id";
  27. pub const SERVER_SETTINGS_REFRESH_FIELD_ID: &str = "server_settings_refresh_field_id";
  28. pub const SERVER_SETTINGS_PASSWORD_FIELD_ID: &str = "SERVER_SETTINGS_PASSWORD_FIELD_ID";
  29. pub const SERVER_SETTINGS_BUTTON_ID: &str = "server_settings_button_id";
  30. pub const DIALOGUE_MIN_SIZE: (u16, u16) = (20, 5);
  31. pub const SERVER_SETTINGS_FIELD_SIZE: (usize, usize) = (60, 1);
  32. pub const USER_PANEL_SIZE: usize = crate::MAX_USERNAME_LENGTH + 2;
  33. #[allow(unused)]
  34. pub enum Labels {
  35. Ok,
  36. Cancel,
  37. Close,
  38. Submit,
  39. Send,
  40. New,
  41. Clear,
  42. Error,
  43. User,
  44. Users,
  45. Message,
  46. Messages,
  47. Channel,
  48. Channels,
  49. NewChannel,
  50. InvalidChannelNameExplination,
  51. TypeAMessage,
  52. SetLanguage,
  53. SetUsername,
  54. InvalidUsernameExplination,
  55. Username,
  56. InvalidMessage,
  57. FailedToSendMessage(NetworkError),
  58. FailedToLoadMessages(NetworkError),
  59. RefreshButton,
  60. BlockedWords,
  61. ServerSettings,
  62. ServerAddress,
  63. ServerRefreshRate,
  64. Password,
  65. }
  66. #[derive(Debug, Clone, Copy)]
  67. pub enum Language {
  68. English,
  69. Dutch,
  70. Japanese,
  71. }
  72. impl Labels {
  73. // TODO: Double check the translations
  74. pub fn localize<'a>(&self, language: Language) -> String {
  75. let buf: [String; 3];
  76. let set: [&str; 3] = match self {
  77. Labels::Ok => ["OK", "OK", "OK"],
  78. Labels::Cancel => ["Cancel", "Annuleren", "キャンセル"],
  79. Labels::Close => ["Close", "Sluiten", "閉じる"],
  80. Labels::Error => ["Error", "Fout", "エラー"],
  81. Labels::User => ["User", "Gebruiker", "ユーザー"],
  82. Labels::Messages => ["Messages", "Berichten", "メッセージ"],
  83. Labels::Message => ["Message", "Bericht", "メッセージ"],
  84. Labels::Users => ["Users", "Gebruikers", "ユーザー"],
  85. Labels::TypeAMessage => [
  86. "Type a message",
  87. "Typ een bericht",
  88. "メッセージを入力してください",
  89. ],
  90. Labels::SetUsername => [
  91. "Set username",
  92. "Stel gebruikersnaam in",
  93. "ユーザー名を設定する",
  94. ],
  95. Labels::Send => ["Send", "Verzenden", "送信する"],
  96. Labels::Clear => ["Clear", "Wissen", "クリア"],
  97. Labels::Username => ["Username", "Gebruikersnaam", "ユーザー名"],
  98. Labels::SetLanguage => ["Set language", "Stel taal in", "言語を設定する"],
  99. Labels::FailedToSendMessage(e) => {
  100. let reason_buf: [String; 3];
  101. match e {
  102. NetworkError::ReqwestError(_) => {
  103. reason_buf = [
  104. "Connection error.".to_string(),
  105. "Verbindingsfout.".to_string(),
  106. "接続エラー。".to_string(),
  107. ];
  108. }
  109. NetworkError::StatusCodeError(code) => {
  110. // NOTE: Needs to match the server
  111. if *code == 400 {
  112. reason_buf = [
  113. "Message too long.".to_string(),
  114. "Bericht te lang.".to_string(),
  115. "メッセージが長すぎます。".to_string(),
  116. ];
  117. } else {
  118. reason_buf = [
  119. format!("Status code: {code}"),
  120. format!("Statuscode: {code}"),
  121. format!("ステータスコード: {code}"),
  122. ]
  123. }
  124. }
  125. NetworkError::InvalidUrl => {
  126. reason_buf = [
  127. "Invalid URL.".to_string(),
  128. "Ongeldig URL.".to_string(),
  129. "無効なURL。".to_string(),
  130. ];
  131. }
  132. NetworkError::EncryptionError(_) => {
  133. reason_buf = [
  134. "Encryption error.".to_string(),
  135. "Encryptiefout.".to_string(),
  136. "暗号化エラー。".to_string(),
  137. ];
  138. }
  139. };
  140. buf = [
  141. format!("Failed to send message. {}", reason_buf[0]),
  142. format!("Bericht verzenden mislukt. {}", reason_buf[1]),
  143. format!("メッセージの送信に失敗しました。 {}", reason_buf[2]),
  144. ];
  145. [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()]
  146. }
  147. Labels::FailedToLoadMessages(error) => {
  148. let reason_buf: [String; 3];
  149. match error {
  150. NetworkError::ReqwestError(_) => {
  151. reason_buf = [
  152. "Connection error.".to_string(),
  153. "Verbindingsfout.".to_string(),
  154. "接続エラー。".to_string(),
  155. ];
  156. }
  157. NetworkError::StatusCodeError(code) => {
  158. reason_buf = [
  159. format!("Status code: {code}"),
  160. format!("Statuscode: {code}"),
  161. format!("ステータスコード: {code}"),
  162. ];
  163. }
  164. NetworkError::InvalidUrl => {
  165. reason_buf = [
  166. "Invalid URL.".to_string(),
  167. "Ongeldig URL.".to_string(),
  168. "無効なURL。".to_string(),
  169. ];
  170. }
  171. NetworkError::EncryptionError(_) => {
  172. reason_buf = [
  173. "Decryption error.".to_string(),
  174. "Fout bij het ontcijferen.".to_string(),
  175. "復号エラー".to_string(),
  176. ];
  177. }
  178. };
  179. buf = [
  180. format!("Failed to load messages. {}", reason_buf[0]),
  181. format!("Berichten laden mislukt. {}", reason_buf[1]),
  182. format!("メッセージの読み込みに失敗しました。 {}", reason_buf[2]),
  183. ];
  184. [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()]
  185. }
  186. Labels::RefreshButton => ["Refresh", "Vernieuwen", "更新する"],
  187. Labels::InvalidUsernameExplination => {
  188. buf = [
  189. r"Invalid username. Must match ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$"
  190. .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
  191. r"Ongeldige gebruikersnaam. Moet overeenkomen met ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$"
  192. .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
  193. r"無効なユーザー名。 ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$ に一致する必要があります"
  194. .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
  195. ];
  196. [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()]
  197. }
  198. Labels::InvalidMessage => {
  199. buf = [
  200. "Invalid message. Must contain fewer than ${MAX_MESSAGE_LENGTH} characters"
  201. .replace("${MAX_MESSAGE_LENGTH}", &MAX_MESSAGE_LENGTH.to_string()),
  202. "Ongeldig bericht. Moet minder dan ${MAX_MESSAGE_LENGTH} tekens bevatten"
  203. .replace("${MAX_MESSAGE_LENGTH}", &MAX_MESSAGE_LENGTH.to_string()),
  204. "無効なメッセージ。 ${MAX_MESSAGE_LENGTH}文字未満である必要があります"
  205. .replace("${MAX_MESSAGE_LENGTH}", &MAX_MESSAGE_LENGTH.to_string()),
  206. ];
  207. [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()]
  208. }
  209. Labels::BlockedWords => [
  210. "Blocked words",
  211. "Geblokkeerde woorden",
  212. "ブロックされた単語",
  213. ],
  214. Labels::Submit => ["Submit", "Indienen", "提出する"],
  215. Labels::ServerSettings => ["Server settings", "Serverinstellingen", "サーバー設定"],
  216. Labels::ServerAddress => [
  217. "Server address (IP:Port)",
  218. "Serveradres (IP:poort)",
  219. "サーバーアドレス(IP:ポート)",
  220. ],
  221. Labels::ServerRefreshRate => [
  222. "Update rate (seconds)",
  223. "Updatefrequentie (seconden)",
  224. "更新率 (秒)",
  225. ],
  226. Labels::Channels => ["Channels", "Kanalen", "チャンネル"],
  227. Labels::NewChannel => ["New Channel", "Nieuw kanaal", "新しいチャンネル"],
  228. Labels::InvalidChannelNameExplination => {
  229. buf = [
  230. r"Invalid channel name. Must match ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$"
  231. .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
  232. r"Ongeldige kanaalnaam. Moet overeenkomen met ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$"
  233. .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
  234. r"無効なチャンネル名。 ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$ に一致する必要があります"
  235. .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()),
  236. ];
  237. [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()]
  238. }
  239. Labels::New => ["New", "Nieuw", "新"],
  240. Labels::Channel => ["Channel", "Kanaal", "チャネル"],
  241. Labels::Password => ["Password", "Wachtwoord", "パスワード"],
  242. };
  243. let idx = match language {
  244. Language::English => 0,
  245. Language::Dutch => 1,
  246. Language::Japanese => 2,
  247. };
  248. set[idx].to_string()
  249. }
  250. }
  251. pub fn alert<S>(siv: &mut Cursive, title: S, text: S)
  252. where
  253. S: Into<String>,
  254. {
  255. let language = get_appdata(siv).language;
  256. keybind_setup_close_once(siv);
  257. siv.add_layer(
  258. Dialog::text(text)
  259. .title(title)
  260. .button(Labels::Close.localize(language), |siv| {
  261. keybind_close_manual_end(siv, false);
  262. })
  263. .min_size((20, 5)),
  264. );
  265. }
  266. pub fn error<S>(siv: &mut Cursive, text: S)
  267. where
  268. S: Into<String>,
  269. {
  270. let language = get_appdata(siv).language;
  271. alert(siv, Labels::Error.localize(language), text.into());
  272. }
  273. /// Sets up hotkey to close the most recent view
  274. pub fn keybind_setup_close_once(siv: &mut Cursive) {
  275. siv.with_user_data(|appdata: &mut Appdata| {
  276. appdata.quick_close_window_count += 1;
  277. });
  278. siv.add_global_callback(event::Key::Esc, |siv| {
  279. keybind_close_manual_end(siv, false);
  280. });
  281. }
  282. // `close_all` can be removed, it isn't used.
  283. /// Manually close the most recent view and removes the hotkey
  284. pub fn keybind_close_manual_end(siv: &mut Cursive, close_all: bool) {
  285. siv.with_user_data(|appdata: &mut Appdata| {
  286. if close_all {
  287. appdata.quick_close_window_count = 0;
  288. } else {
  289. appdata.quick_close_window_count = appdata.quick_close_window_count.saturating_sub(1);
  290. }
  291. });
  292. if get_appdata(siv).quick_close_window_count == 0 {
  293. siv.clear_global_callbacks(event::Key::Esc);
  294. }
  295. siv.pop_layer();
  296. }
  297. pub fn visual_update(siv: &mut Cursive) {
  298. let appdata = get_appdata(siv);
  299. let current_channel = appdata.current_channel;
  300. let language = get_appdata(siv).language;
  301. // --- Messages ---
  302. let mut messages: Vec<message::MessageSanitized> = appdata
  303. .messages
  304. .clone()
  305. .values()
  306. .cloned()
  307. .map(MessageSanitized::from)
  308. .collect::<Vec<_>>();
  309. messages.sort_by(|a, b| a.time.cmp(&b.time));
  310. // Remove blocked phrases
  311. for message in messages.iter_mut() {
  312. message.remove_blocked_phrases(siv);
  313. }
  314. siv.call_on_name(
  315. MESSAGE_PANEL_ID,
  316. |panel: &mut Panel<ScrollView<SelectView>>| {
  317. let view = panel.get_inner_mut().get_inner_mut();
  318. let selected = view.selected_id();
  319. view.clear();
  320. for message in messages
  321. .iter()
  322. .filter(|x| channel_matches(&x.channel, &current_channel))
  323. {
  324. view.add_item(
  325. // TODO: Localized timestamps
  326. format!(
  327. "{:>6} | {}: {}\n",
  328. utils::time::timestamp_relative(message.time),
  329. message.sender,
  330. message.content,
  331. ),
  332. message.hash().to_string(),
  333. );
  334. }
  335. if let Some(selected) = selected {
  336. view.set_selection(selected);
  337. }
  338. },
  339. );
  340. // --- Members list ---
  341. siv.call_on_name(
  342. USERS_PANEL_ID,
  343. |panel: &mut Panel<ScrollView<SelectView>>| {
  344. let view = panel.get_inner_mut().get_inner_mut();
  345. let mut senders: Vec<String> = vec![];
  346. for message in messages
  347. .iter()
  348. .filter(|x| channel_matches(&x.channel, &current_channel))
  349. {
  350. if senders.contains(&message.sender) {
  351. continue;
  352. }
  353. senders.push(message.sender.clone());
  354. }
  355. senders.sort();
  356. let selected = view.selected_id();
  357. view.clear();
  358. for sender in senders {
  359. view.add_item(sender.clone(), sender.clone());
  360. }
  361. if let Some(selected) = selected {
  362. view.set_selection(selected);
  363. }
  364. },
  365. );
  366. // --- Channels ---
  367. siv.call_on_name(
  368. CHANNEL_VIEW_ID,
  369. |panel: &mut Panel<ScrollView<SelectView>>| {
  370. let view = panel.get_inner_mut().get_inner_mut();
  371. let mut channels = appdata.local_channels.clone();
  372. for message in messages.iter() {
  373. if !channels.contains(&message.channel) {
  374. channels.push(message.channel.clone());
  375. }
  376. }
  377. channels.sort();
  378. let selected = view.selected_id();
  379. view.clear();
  380. for channel in channels {
  381. view.add_item(channel.clone(), channel.clone());
  382. }
  383. if let Some(selected) = selected {
  384. view.set_selection(selected);
  385. }
  386. },
  387. );
  388. // --- Current channel ---
  389. let appdata_c = get_appdata(siv);
  390. siv.call_on_name(
  391. CHANNEL_CURRENT_PANEL_ID,
  392. |view: &mut NamedView<Panel<TextView>>| {
  393. view.get_mut()
  394. .get_inner_mut()
  395. .set_content(appdata_c.current_channel);
  396. view.get_mut().set_title(Labels::Channel.localize(language));
  397. },
  398. );
  399. // --- Current username ---
  400. siv.call_on_name(
  401. CURRENT_USERNAME_PANEL_ID,
  402. |view: &mut NamedView<Panel<TextView>>| {
  403. view.get_mut().get_inner_mut().set_content(appdata.username);
  404. },
  405. );
  406. // --- Localize buttons ---
  407. for (name, label) in [
  408. (USERS_PANEL_ID, Labels::Users.localize(language)),
  409. (MESSAGE_PANEL_ID, Labels::Messages.localize(language)),
  410. (CHANNEL_VIEW_ID, Labels::Channels.localize(language)),
  411. ] {
  412. siv.call_on_name(name, |panel: &mut Panel<ScrollView<SelectView>>| {
  413. panel.set_title(label);
  414. });
  415. }
  416. siv.call_on_name(CHANNEL_NEW_BUTTON_ID, |view: &mut NamedView<Button>| {
  417. view.get_mut()
  418. .set_label(Labels::NewChannel.localize(language));
  419. });
  420. siv.call_on_name(INPUT_PANEL_ID, |panel: &mut Panel<LinearLayout>| {
  421. panel.set_title(Labels::TypeAMessage.localize(language));
  422. });
  423. siv.call_on_name(INPUT_BUTTON_ID, |view: &mut NamedView<Button>| {
  424. view.get_mut().set_label(Labels::Send.localize(language));
  425. });
  426. siv.call_on_name(INPUT_CLEAR_BUTTON_ID, |view: &mut NamedView<Button>| {
  427. view.get_mut().set_label(Labels::Clear.localize(language));
  428. });
  429. siv.call_on_name(LANGUAGE_BUTTON_ID, |view: &mut NamedView<Button>| {
  430. view.get_mut()
  431. .set_label(Labels::SetLanguage.localize(language));
  432. });
  433. siv.call_on_name(USERNAME_BUTTON_ID, |view: &mut NamedView<Button>| {
  434. view.get_mut()
  435. .set_label(Labels::SetUsername.localize(language));
  436. });
  437. siv.call_on_name(REFRESH_BUTTON_ID, |view: &mut NamedView<Button>| {
  438. view.get_mut()
  439. .set_label(Labels::RefreshButton.localize(language));
  440. });
  441. siv.call_on_name(BLOCKED_WORDS_BUTTON_ID, |view: &mut NamedView<Button>| {
  442. view.get_mut()
  443. .set_label(Labels::BlockedWords.localize(language));
  444. });
  445. siv.call_on_name(
  446. CURRENT_USERNAME_PANEL_ID,
  447. |view: &mut NamedView<Panel<TextView>>| {
  448. view.get_mut()
  449. .set_title(Labels::Username.localize(language));
  450. },
  451. );
  452. siv.call_on_name(SERVER_SETTINGS_BUTTON_ID, |view: &mut NamedView<Button>| {
  453. view.get_mut()
  454. .set_label(Labels::ServerSettings.localize(language));
  455. });
  456. }
  457. pub fn change_language(siv: &mut Cursive, language: Language) {
  458. siv.with_user_data(|appdata: &mut Appdata| {
  459. appdata.language = language;
  460. })
  461. .expect("Failed to set language.");
  462. visual_update(siv);
  463. }
  464. pub fn channel_matches(message_channel: &str, reference_channel: &str) -> bool {
  465. message_channel == reference_channel
  466. }