mod.rs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use std::{fs::File, io::Write};
  19. use darkfi::system::msleep;
  20. use darkfi_serial::{deserialize, Encodable};
  21. use indoc::indoc;
  22. use kvdb_overlay::Database;
  23. use crate::{
  24. app::{
  25. node::{create_button, create_layer, create_text, create_vector_art, create_video},
  26. App,
  27. },
  28. expr::{self, Compiler},
  29. gfx::gfxtag,
  30. prop::{PropertyAtomicGuard, PropertyFloat32, Role},
  31. scene::{SceneNodePtr, Slot},
  32. sfx, shape,
  33. ui::{emoji_picker, Button, Layer, Text, VectorArt, VectorShape, Video},
  34. util::i18n::I18nBabelFish,
  35. };
  36. mod chat;
  37. pub mod menu;
  38. use menu::channel::Channel;
  39. pub mod test;
  40. pub mod test_edit;
  41. pub mod test_scroll_layer;
  42. mod wallet;
  43. macro_rules! i { ($($arg:tt)*) => { info!(target: "app::schema", $($arg)*); } }
  44. macro_rules! e { ($($arg:tt)*) => { error!(target: "app::schema", $($arg)*); } }
  45. const COLOR_SCHEME: ColorScheme = ColorScheme::DarkMode;
  46. //const COLOR_SCHEME: ColorScheme = ColorScheme::PaperLight;
  47. #[cfg(any(target_os = "android", feature = "emulate-android"))]
  48. mod android_ui_consts {
  49. pub const NETSTATUS_ICON_SIZE: f32 = 140.;
  50. pub const SETTINGS_ICON_SIZE: f32 = 140.;
  51. pub const NETLOGO_SCALE: f32 = 50.;
  52. pub const EMOJI_PICKER_ICON_SIZE: f32 = 120.;
  53. pub const EMOJI_PICKER_ICON_MARGIN_X: f32 = 20.;
  54. pub const EMOJI_PICKER_ICON_MARGIN_Y: f32 = 20.;
  55. pub const NETSTAT_OVERLAY_MARGIN: f32 = 20.;
  56. pub const NETSTAT_OVERLAY_BTN_W: f32 = 200.;
  57. pub const NETSTAT_OVERLAY_BTN_H: f32 = 90.;
  58. pub const NETSTAT_OVERLAY_BTN_FONTSIZE: f32 = 40.;
  59. }
  60. #[cfg(target_os = "android")]
  61. mod ui_consts {
  62. use crate::android::{get_appdata_path, get_external_storage_path};
  63. use std::path::PathBuf;
  64. pub const VID_PATH: &str = "forest_720x1280.mp4";
  65. pub const VID_ASPECT_RATIO: f32 = 9. / 16.;
  66. pub use super::android_ui_consts::*;
  67. pub fn get_chatdb_path() -> PathBuf {
  68. get_external_storage_path().join("chatdb")
  69. }
  70. pub fn get_main_db_path() -> PathBuf {
  71. get_appdata_path().join("db")
  72. }
  73. pub fn get_joined_channels_filename() -> PathBuf {
  74. get_appdata_path().join("joined.txt")
  75. }
  76. }
  77. #[cfg(not(target_os = "android"))]
  78. mod desktop_paths {
  79. use std::path::PathBuf;
  80. pub const VID_PATH: &str = "assets/forest_1920x1080.ivf";
  81. pub const VID_ASPECT_RATIO: f32 = 16. / 9.;
  82. pub fn get_chatdb_path() -> PathBuf {
  83. dirs::data_local_dir().unwrap().join("darkfi/app/chatdb")
  84. }
  85. pub fn get_main_db_path() -> PathBuf {
  86. dirs::data_local_dir().unwrap().join("darkfi/app/db")
  87. }
  88. pub fn get_joined_channels_filename() -> PathBuf {
  89. dirs::cache_dir().unwrap().join("darkfi/app/joined.txt")
  90. }
  91. }
  92. #[cfg(feature = "emulate-android")]
  93. mod ui_consts {
  94. pub use super::{android_ui_consts::*, desktop_paths::*};
  95. }
  96. #[cfg(all(
  97. any(target_os = "linux", target_os = "macos", target_os = "windows"),
  98. not(feature = "emulate-android")
  99. ))]
  100. mod ui_consts {
  101. pub const NETSTATUS_ICON_SIZE: f32 = 60.;
  102. pub const SETTINGS_ICON_SIZE: f32 = 60.;
  103. pub const NETLOGO_SCALE: f32 = 25.;
  104. pub const EMOJI_PICKER_ICON_SIZE: f32 = 40.;
  105. pub const EMOJI_PICKER_ICON_MARGIN_X: f32 = 8.;
  106. pub const EMOJI_PICKER_ICON_MARGIN_Y: f32 = 8.;
  107. pub const NETSTAT_OVERLAY_MARGIN: f32 = 10.;
  108. pub const NETSTAT_OVERLAY_BTN_W: f32 = 100.;
  109. pub const NETSTAT_OVERLAY_BTN_H: f32 = 45.;
  110. pub const NETSTAT_OVERLAY_BTN_FONTSIZE: f32 = 20.;
  111. pub use super::desktop_paths::*;
  112. }
  113. pub use ui_consts::*;
  114. pub static DEFAULT_CHANNELS: &'static [&str] =
  115. &["dev", "media", "hackers", "memes", "philosophy", "markets", "math", "random"];
  116. #[derive(PartialEq)]
  117. enum ColorScheme {
  118. DarkMode,
  119. PaperLight,
  120. }
  121. /// Read the ordered list of joined channels/contacts (prefixed names like "#dev", "@alice").
  122. pub fn read_joined_channels() -> Vec<String> {
  123. let Ok(contents) = std::fs::read_to_string(get_joined_channels_filename()) else {
  124. return vec![]
  125. };
  126. let mut joined = vec![];
  127. for line in contents.lines() {
  128. let line = line.trim();
  129. assert!(!line.is_empty());
  130. joined.push(line.to_string());
  131. }
  132. joined
  133. }
  134. /// First-run seed: write DEFAULT_CHANNELS (as "#name") if no joined file exists.
  135. /// Idempotent and deterministic, so it is safe to call from both schema startup and the plugin.
  136. pub fn ensure_joined_channels_seeded() {
  137. let path = get_joined_channels_filename();
  138. if path.exists() {
  139. return
  140. }
  141. if let Some(parent) = path.parent() {
  142. let _ = std::fs::create_dir_all(parent);
  143. }
  144. let defaults: Vec<String> = DEFAULT_CHANNELS.iter().map(|c| format!("#{}", c)).collect();
  145. let _ = std::fs::write(&path, defaults.join("\n"));
  146. }
  147. /// Append a single channel line to the joined file (no dedup). Ensures newline separation
  148. /// when appending to a non-empty file that lacks a trailing newline.
  149. pub fn write_joined_channel(name: &str) {
  150. let path = get_joined_channels_filename();
  151. let needs_newline = std::fs::read(&path)
  152. .map(|bytes| !bytes.is_empty() && !bytes.ends_with(b"\n"))
  153. .unwrap_or(false);
  154. let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&path).unwrap();
  155. if needs_newline {
  156. writeln!(f).unwrap();
  157. }
  158. writeln!(f, "{}", name).unwrap();
  159. }
  160. /// Join a channel: skip if already joined, otherwise append one line.
  161. pub fn append_joined_channel(name: &str) {
  162. if read_joined_channels().iter().any(|x| x == name) {
  163. return
  164. }
  165. write_joined_channel(name);
  166. }
  167. /// Rewrite the whole joined list (used for first-run seed and main-menu edit_done sync).
  168. pub fn write_joined_channels(items: &[String]) {
  169. let _ = std::fs::write(get_joined_channels_filename(), items.join("\n"));
  170. }
  171. pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db: Database) {
  172. let mut cc = Compiler::new();
  173. cc.add_const_f32("NETSTATUS_ICON_SIZE", NETSTATUS_ICON_SIZE);
  174. cc.add_const_f32("SETTINGS_ICON_SIZE", SETTINGS_ICON_SIZE);
  175. cc.add_const_f32("NETSTAT_OVERLAY_MARGIN", NETSTAT_OVERLAY_MARGIN);
  176. cc.add_const_f32("NETSTAT_OVERLAY_BTN_W", NETSTAT_OVERLAY_BTN_W);
  177. cc.add_const_f32("NETSTAT_OVERLAY_BTN_H", NETSTAT_OVERLAY_BTN_H);
  178. let atom = &mut PropertyAtomicGuard::none();
  179. let window_scale = PropertyFloat32::wrap(
  180. &app.sg_root.lookup_node("/window").unwrap(),
  181. Role::Internal,
  182. "scale",
  183. 0,
  184. )
  185. .unwrap();
  186. // Root content layer
  187. let content = create_layer("content");
  188. let prop = content.get_property("rect").unwrap();
  189. prop.set_expr(atom, Role::App, 0, expr::load_var("insets_left")).unwrap();
  190. prop.set_expr(atom, Role::App, 1, expr::load_var("insets_top")).unwrap();
  191. let code = cc.compile("w - insets_left - insets_right").unwrap();
  192. prop.set_expr(atom, Role::App, 2, code).unwrap();
  193. let code = cc.compile("h - insets_top - insets_bottom").unwrap();
  194. prop.set_expr(atom, Role::App, 3, code).unwrap();
  195. let window_insets = window.get_property("insets").unwrap();
  196. prop.add_depend(&window_insets, 0, "insets_left");
  197. prop.add_depend(&window_insets, 1, "insets_top");
  198. prop.add_depend(&window_insets, 2, "insets_right");
  199. prop.add_depend(&window_insets, 3, "insets_bottom");
  200. content.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
  201. content.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
  202. let content =
  203. content.setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
  204. window.link(content.clone());
  205. if COLOR_SCHEME == ColorScheme::DarkMode {
  206. let node = create_video("king");
  207. let prop = node.get_property("rect").unwrap();
  208. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  209. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  210. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  211. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  212. cc.add_const_f32("R", VID_ASPECT_RATIO);
  213. let prop = node.get_property("uv").unwrap();
  214. #[rustfmt::skip]
  215. let code = cc.compile(indoc! {"
  216. r = w / h;
  217. # r < R means screen narrower than image
  218. if r < R {
  219. 0.5 - (r / (2 * R))
  220. } else {
  221. 0
  222. }
  223. "}).unwrap();
  224. prop.set_expr(atom, Role::App, 0, code).unwrap();
  225. #[rustfmt::skip]
  226. let code = cc.compile(indoc! {"
  227. r = w / h;
  228. if r < R {
  229. 0
  230. } else {
  231. 0.5 - (R / (2 * r))
  232. }
  233. "}).unwrap();
  234. prop.set_expr(atom, Role::App, 1, code).unwrap();
  235. #[rustfmt::skip]
  236. let code = cc.compile(indoc! {"
  237. r = w / h;
  238. if r < R {
  239. r / R
  240. } else {
  241. 1
  242. }
  243. "}).unwrap();
  244. prop.set_expr(atom, Role::App, 2, code).unwrap();
  245. #[rustfmt::skip]
  246. let code = cc.compile(indoc! {"
  247. r = w / h;
  248. if r < R {
  249. 1
  250. } else {
  251. R / r
  252. }
  253. "}).unwrap();
  254. prop.set_expr(atom, Role::App, 3, code).unwrap();
  255. //node.set_property_str(atom, Role::App, "path", BG_PATH).unwrap();
  256. node.set_property_str(atom, Role::App, "path", VID_PATH).unwrap();
  257. node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
  258. //let node = node.setup(|me| Image::new(me, app.renderer.clone())).await;
  259. //layer_node.link(node);
  260. let node = node
  261. .setup(|me| {
  262. Video::new(me, app.renderer.clone(), app.redraw_trigger.clone(), app.ex.clone())
  263. })
  264. .await;
  265. content.link(node);
  266. } else if COLOR_SCHEME == ColorScheme::PaperLight {
  267. let node = create_vector_art("bg");
  268. let prop = node.get_property("rect").unwrap();
  269. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  270. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  271. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  272. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  273. node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
  274. let c = 1.;
  275. // Setup the pimpl
  276. let mut shape = VectorShape::new();
  277. shape.add_filled_box(
  278. expr::const_f32(0.),
  279. expr::const_f32(0.),
  280. expr::load_var("w"),
  281. expr::load_var("h"),
  282. [c, c, c, 0.3],
  283. );
  284. let node = node
  285. .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
  286. .await;
  287. window.link(node);
  288. }
  289. let emoji_meshes = emoji_picker::EmojiMeshes::new(app.renderer.clone(), EMOJI_PICKER_ICON_SIZE);
  290. let emoji_meshes2 = emoji_meshes.clone();
  291. let _ = std::thread::spawn(move || {
  292. for i in (0..500).step_by(20) {
  293. for j in i..(i + 20) {
  294. emoji_meshes2.lock().get(j);
  295. }
  296. }
  297. });
  298. let channels_tree = db.open_tree_default("channels").expect("cannot open channels tree");
  299. let contacts_tree = db.open_tree_default("contacts").expect("cannot open contacts tree");
  300. // Initialize default channels if tree is empty
  301. if channels_tree.is_empty().unwrap() {
  302. for channel_name in DEFAULT_CHANNELS {
  303. let channel = Channel { name: channel_name.to_string(), secret: None };
  304. let mut val = vec![];
  305. channel.encode(&mut val).unwrap();
  306. channels_tree.insert(channel_name.to_string().as_bytes(), &val).unwrap();
  307. }
  308. }
  309. // Seed the joined-channels file with defaults on first run.
  310. ensure_joined_channels_seeded();
  311. // Create chat container layer
  312. let chat_layer = create_layer("chat");
  313. let prop = chat_layer.get_property("rect").unwrap();
  314. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  315. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  316. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  317. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  318. chat_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
  319. chat_layer.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
  320. let chat_layer = chat_layer
  321. .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
  322. .await;
  323. content.link(chat_layer.clone());
  324. let netlayer_node = create_layer("netstatus_layer");
  325. let prop = netlayer_node.get_property("rect").unwrap();
  326. let code = cc.compile("w - NETSTATUS_ICON_SIZE").unwrap();
  327. prop.set_expr(atom, Role::App, 0, code).unwrap();
  328. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  329. //prop.set_f32(atom, Role::App, 2, NETSTATUS_ICON_SIZE).unwrap();
  330. //prop.set_f32(atom, Role::App, 3, NETSTATUS_ICON_SIZE).unwrap();
  331. prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
  332. prop.set_f32(atom, Role::App, 3, 1000.).unwrap();
  333. netlayer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
  334. netlayer_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  335. netlayer_node.set_property_u32(atom, Role::App, "priority", 1).unwrap();
  336. let netlayer_node = netlayer_node
  337. .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
  338. .await;
  339. chat_layer.link(netlayer_node.clone());
  340. let node = create_vector_art("net0");
  341. let prop = node.get_property("rect").unwrap();
  342. prop.set_f32(atom, Role::App, 0, NETSTATUS_ICON_SIZE / 2.).unwrap();
  343. prop.set_f32(atom, Role::App, 1, NETSTATUS_ICON_SIZE / 2.).unwrap();
  344. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  345. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  346. node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
  347. node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
  348. node.set_property_f32(atom, Role::App, "scale", NETLOGO_SCALE).unwrap();
  349. let mut shape = shape::create_netlogo1([1., 0., 0.25, 1.]);
  350. shape.join(shape::create_netlogo2([0.27, 0.4, 0.4, 1.]));
  351. shape.join(shape::create_netlogo3([0.27, 0.4, 0.4, 1.]));
  352. let net0_node = node
  353. .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
  354. .await;
  355. netlayer_node.link(net0_node);
  356. let node = create_vector_art("net1");
  357. let prop = node.get_property("rect").unwrap();
  358. prop.set_f32(atom, Role::App, 0, NETSTATUS_ICON_SIZE / 2.).unwrap();
  359. prop.set_f32(atom, Role::App, 1, NETSTATUS_ICON_SIZE / 2.).unwrap();
  360. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  361. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  362. node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
  363. node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
  364. node.set_property_f32(atom, Role::App, "scale", NETLOGO_SCALE).unwrap();
  365. let mut shape = shape::create_netlogo1([0.49, 0.57, 1., 1.]);
  366. shape.join(shape::create_netlogo2([0.49, 0.57, 1., 1.]));
  367. shape.join(shape::create_netlogo3([0.27, 0.4, 0.4, 1.]));
  368. let net1_node = node
  369. .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
  370. .await;
  371. netlayer_node.link(net1_node);
  372. let node = create_vector_art("net2");
  373. let prop = node.get_property("rect").unwrap();
  374. prop.set_f32(atom, Role::App, 0, NETSTATUS_ICON_SIZE / 2.).unwrap();
  375. prop.set_f32(atom, Role::App, 1, NETSTATUS_ICON_SIZE / 2.).unwrap();
  376. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  377. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  378. node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
  379. node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
  380. node.set_property_f32(atom, Role::App, "scale", NETLOGO_SCALE).unwrap();
  381. let mut shape = shape::create_netlogo1([0., 0.94, 1., 1.]);
  382. shape.join(shape::create_netlogo2([0., 0.94, 1., 1.]));
  383. shape.join(shape::create_netlogo3([0., 0.94, 1., 1.]));
  384. let net2_node = node
  385. .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
  386. .await;
  387. netlayer_node.link(net2_node);
  388. let node = create_vector_art("net3");
  389. let prop = node.get_property("rect").unwrap();
  390. prop.set_f32(atom, Role::App, 0, NETSTATUS_ICON_SIZE / 2.).unwrap();
  391. prop.set_f32(atom, Role::App, 1, NETSTATUS_ICON_SIZE / 2.).unwrap();
  392. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  393. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  394. node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
  395. node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
  396. node.set_property_f32(atom, Role::App, "scale", NETLOGO_SCALE).unwrap();
  397. let mut shape = shape::create_netlogo1([0., 0.94, 1., 1.]);
  398. shape.join(shape::create_netlogo2([0., 0.94, 1., 1.]));
  399. shape.join(shape::create_netlogo3([0., 0.94, 1., 1.]));
  400. let net3_node = node
  401. .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
  402. .await;
  403. netlayer_node.link(net3_node);
  404. // netstat-klik icon (visual feedback when reconnect button is clicked)
  405. let klik_color = [0., 0.5, 1., 1.]; // Blue
  406. let node = create_vector_art("netstat_klik");
  407. let prop = node.get_property("rect").unwrap();
  408. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  409. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  410. prop.set_f32(atom, Role::App, 2, NETSTATUS_ICON_SIZE).unwrap();
  411. prop.set_f32(atom, Role::App, 3, NETSTATUS_ICON_SIZE).unwrap();
  412. node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
  413. // Above other icons
  414. node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
  415. let mut shape = VectorShape::new();
  416. shape.add_filled_box(
  417. expr::const_f32(0.),
  418. expr::const_f32(0.),
  419. expr::const_f32(NETSTATUS_ICON_SIZE),
  420. expr::const_f32(NETSTATUS_ICON_SIZE),
  421. klik_color,
  422. );
  423. let netstat_klik_node = node
  424. .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
  425. .await;
  426. netlayer_node.link(netstat_klik_node.clone());
  427. // Reconnect Button (overlaid on netstatus icons)
  428. let node = create_button("reconnect_btn");
  429. node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
  430. let prop = node.get_property("rect").unwrap();
  431. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  432. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  433. prop.set_f32(atom, Role::App, 2, NETSTATUS_ICON_SIZE).unwrap();
  434. prop.set_f32(atom, Role::App, 3, NETSTATUS_ICON_SIZE).unwrap();
  435. let sg_root = app.sg_root.clone();
  436. let redraw = app.redraw_trigger.clone();
  437. let (slot, recvr) = Slot::new("reconnect_clicked");
  438. node.register("click", slot).unwrap();
  439. let reconnect_task = app.ex.spawn(async move {
  440. while let Ok(_) = recvr.recv().await {
  441. i!("Reconnect button clicked");
  442. // Toggle the overlay layer
  443. let overlay = sg_root.lookup_node("/window/content/chat/netstatus_overlay").unwrap();
  444. let is_visible = overlay.get_property_bool("is_visible").unwrap();
  445. if !is_visible {
  446. sfx::play_cloak();
  447. }
  448. let atom = &mut redraw.make_guard(gfxtag!("netstatus overlay toggle"));
  449. overlay.set_property_bool(atom, Role::App, "is_visible", !is_visible).unwrap();
  450. /*
  451. // Show netstat-klik icon
  452. let netstat_klik =
  453. sg_root.lookup_node("/window/content/chat/netstatus_layer/netstat_klik").unwrap();
  454. {
  455. let atom = &mut redraw.make_guard(gfxtag!("netstat_klik_show"));
  456. if let Err(e) = netstat_klik.set_property_bool(atom, Role::App, "is_visible", true)
  457. {
  458. e!("Failed to show netstat_klik: {e}");
  459. }
  460. }
  461. // Trigger reconnect
  462. match sg_root.lookup_node("/plugin/darkirc") {
  463. Some(darkirc) => {
  464. if let Err(e) = darkirc.call_method("reconnect", vec![]).await {
  465. e!("Failed to trigger reconnect: {e}");
  466. }
  467. }
  468. None => {
  469. e!("DarkIrc plugin has not been loaded");
  470. }
  471. }
  472. msleep(200).await;
  473. // Hide netstat-klik icon
  474. {
  475. let atom = &mut redraw.make_guard(gfxtag!("netstat_klik_hide"));
  476. if let Err(e) = netstat_klik.set_property_bool(atom, Role::App, "is_visible", false)
  477. {
  478. e!("Failed to hide netstat_klik: {e}");
  479. }
  480. }
  481. */
  482. }
  483. });
  484. app.tasks.lock().unwrap().push(reconnect_task);
  485. let node =
  486. node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
  487. netlayer_node.link(node);
  488. // Overlay layer toggled by the netstatus logo. Sits on top of everything
  489. // except the header strip, so the logo stays visible and clickable.
  490. let overlay_node = create_layer("netstatus_overlay");
  491. let prop = overlay_node.get_property("rect").unwrap();
  492. prop.set_f32(atom, Role::App, 0, NETSTAT_OVERLAY_MARGIN).unwrap();
  493. prop.set_f32(atom, Role::App, 1, NETSTATUS_ICON_SIZE + NETSTAT_OVERLAY_MARGIN).unwrap();
  494. let code = cc.compile("w - 2 * NETSTAT_OVERLAY_MARGIN").unwrap();
  495. prop.set_expr(atom, Role::App, 2, code).unwrap();
  496. let code = cc.compile("h - NETSTATUS_ICON_SIZE - 2 * NETSTAT_OVERLAY_MARGIN").unwrap();
  497. prop.set_expr(atom, Role::App, 3, code).unwrap();
  498. overlay_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
  499. overlay_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  500. let overlay_node = overlay_node
  501. .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
  502. .await;
  503. chat_layer.link(overlay_node.clone());
  504. // Placeholder single-color background filling the whole overlay
  505. let node = create_vector_art("overlay_bg");
  506. let prop = node.get_property("rect").unwrap();
  507. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  508. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  509. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  510. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  511. node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
  512. node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
  513. let overlay_color = [0.05, 0.25, 0.3, 0.8];
  514. let mut shape = VectorShape::new();
  515. shape.add_filled_box(
  516. expr::const_f32(0.),
  517. expr::const_f32(0.),
  518. expr::load_var("w"),
  519. expr::load_var("h"),
  520. overlay_color,
  521. );
  522. let node = node
  523. .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
  524. .await;
  525. overlay_node.link(node);
  526. let node = create_text("info");
  527. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  528. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  529. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  530. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  531. node.set_property_f32(atom, Role::App, "font_size", NETSTAT_OVERLAY_BTN_FONTSIZE).unwrap();
  532. /*
  533. #[cfg(target_os = "android")]
  534. {
  535. let info = crate::android::get_display_debug_info();
  536. i!("Display debug report:\n{info}");
  537. node.set_property_str(atom, Role::App, "text", info).unwrap();
  538. }
  539. #[cfg(not(target_os = "android"))]
  540. {
  541. let info = indoc! {"
  542. nothing to see here
  543. folx
  544. hello
  545. "};
  546. node.set_property_str(atom, Role::App, "text", info).unwrap();
  547. }
  548. */
  549. node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
  550. let prop = node.get_property("text_color").unwrap();
  551. prop.set_f32(atom, Role::App, 0, 1.).unwrap();
  552. prop.set_f32(atom, Role::App, 1, 1.).unwrap();
  553. prop.set_f32(atom, Role::App, 2, 1.).unwrap();
  554. prop.set_f32(atom, Role::App, 3, 1.).unwrap();
  555. node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  556. let node = node
  557. .setup(|me| {
  558. Text::new(
  559. me,
  560. window_scale.clone(),
  561. app.renderer.clone(),
  562. i18n_fish.clone(),
  563. app.redraw_trigger.clone(),
  564. )
  565. })
  566. .await;
  567. overlay_node.link(node);
  568. // Stop/start p2p button
  569. let node = create_button("copy_btn");
  570. node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
  571. let prop = node.get_property("rect").unwrap();
  572. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  573. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  574. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  575. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  576. let sg_root = app.sg_root.clone();
  577. let redraw = app.redraw_trigger.clone();
  578. let (slot, recvr) = Slot::new("stop_btn_clicked");
  579. node.register("click", slot).unwrap();
  580. let stop_task = app.ex.spawn(async move {
  581. let mut p2p_running = true;
  582. while let Ok(_) = recvr.recv().await {
  583. /*
  584. #[cfg(target_os = "android")]
  585. {
  586. let info = crate::android::get_display_debug_info();
  587. crate::clipboard::set(&info);
  588. i!("Copied report!");
  589. }
  590. */
  591. }
  592. });
  593. app.tasks.lock().unwrap().push(stop_task);
  594. let node =
  595. node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
  596. overlay_node.link(node);
  597. menu::make(
  598. app,
  599. chat_layer.clone(),
  600. i18n_fish,
  601. channels_tree.clone(),
  602. contacts_tree.clone(),
  603. &db,
  604. emoji_meshes.clone(),
  605. )
  606. .await;
  607. // Create chat layers only for joined channels/contacts, in joined order.
  608. for name in read_joined_channels() {
  609. let bare = if name.starts_with('#') || name.starts_with('@') { &name[1..] } else { &name };
  610. let in_tree = match name.chars().next() {
  611. Some('#') => channels_tree.contains_key(bare.to_string().as_bytes()).unwrap_or(false),
  612. Some('@') => contacts_tree.contains_key(bare.to_string().as_bytes()).unwrap_or(false),
  613. _ => false,
  614. };
  615. if !in_tree {
  616. warn!(target: "app::schema", "Joined entry '{name}' not found in tree; skipping");
  617. continue
  618. }
  619. chat::make(
  620. &app.sg_root,
  621. &app.renderer,
  622. &app.ex,
  623. chat_layer.clone(),
  624. &name,
  625. &db,
  626. i18n_fish,
  627. emoji_meshes.clone(),
  628. app.redraw_trigger.clone(),
  629. )
  630. .await;
  631. }
  632. wallet::make(app, content.clone(), i18n_fish).await;
  633. // Setup wallet button after wallet layer is created
  634. menu::setup_wallet_button(app, chat_layer, i18n_fish).await;
  635. // @@@ Debug stuff @@@
  636. //let chatview_node = app.sg_root.lookup_node("/window/content/chat/dev_chat_layer").unwrap();
  637. //chatview_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
  638. //let menu_node = app.sg_root.lookup_node("/window/content/chat/menu_layer").unwrap();
  639. //menu_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
  640. }