Просмотр исходного кода

app: store the channel order in main menu which is separate from contacts/channels trees

jkds 1 неделя назад
Родитель
Сommit
fab46808fa

+ 6 - 2
bin/app/src/app/schema/menu/channel.rs

@@ -33,8 +33,10 @@ pub struct Channel {
 }
 }
 
 
 use super::{
 use super::{
-    super::chat, edit_buttons, edit_switch::edit_switch, ColorScheme, BTN_TEXT_Y,
-    CHANNEL_ITEM_HEIGHT, COLOR_SCHEME, MENU_BTN_W_L,
+    super::{append_joined_channel, chat},
+    edit_buttons,
+    edit_switch::edit_switch,
+    ColorScheme, BTN_TEXT_Y, CHANNEL_ITEM_HEIGHT, COLOR_SCHEME, MENU_BTN_W_L,
 };
 };
 use crate::{
 use crate::{
     app::{
     app::{
@@ -1464,6 +1466,8 @@ pub async fn make(
                 items_prop.push_str(atom, Role::App, &channel).unwrap();
                 items_prop.push_str(atom, Role::App, &channel).unwrap();
             }
             }
 
 
+            append_joined_channel(&channel);
+
             // Hide channel screen
             // Hide channel screen
             channel_vis.set(atom, false);
             channel_vis.set(atom, false);
 
 

+ 6 - 2
bin/app/src/app/schema/menu/contact.rs

@@ -34,8 +34,10 @@ pub struct Contact {
 }
 }
 
 
 use super::{
 use super::{
-    super::chat, edit_buttons, edit_switch::edit_switch, ColorScheme, BTN_TEXT_Y,
-    CHANNEL_ITEM_HEIGHT, COLOR_SCHEME, MENU_BTN_W_L,
+    super::{append_joined_channel, chat},
+    edit_buttons,
+    edit_switch::edit_switch,
+    ColorScheme, BTN_TEXT_Y, CHANNEL_ITEM_HEIGHT, COLOR_SCHEME, MENU_BTN_W_L,
 };
 };
 use crate::{
 use crate::{
     app::{
     app::{
@@ -1455,6 +1457,8 @@ pub async fn make(
                 items_prop.push_str(atom, Role::App, &contact).unwrap();
                 items_prop.push_str(atom, Role::App, &contact).unwrap();
             }
             }
 
 
+            append_joined_channel(&contact);
+
             contact_vis.set(atom, false);
             contact_vis.set(atom, false);
 
 
             let win = sg_root.lookup_node("/window").unwrap();
             let win = sg_root.lookup_node("/window").unwrap();

+ 26 - 20
bin/app/src/app/schema/menu/mod.rs

@@ -18,9 +18,10 @@
 
 
 use darkfi_serial::deserialize;
 use darkfi_serial::deserialize;
 use sled_overlay::sled;
 use sled_overlay::sled;
+use std::io::Write;
 use ui_consts::*;
 use ui_consts::*;
 
 
-use super::{ColorScheme, COLOR_SCHEME};
+use super::{read_joined_channels, write_joined_channels, ColorScheme, COLOR_SCHEME};
 use crate::{
 use crate::{
     app::{
     app::{
         node::{create_button, create_layer, create_menu, create_text, create_vector_art},
         node::{create_button, create_layer, create_menu, create_text, create_vector_art},
@@ -38,8 +39,6 @@ use crate::{
     },
     },
     util::i18n::I18nBabelFish,
     util::i18n::I18nBabelFish,
 };
 };
-use channel::Channel;
-use contact::Contact;
 
 
 #[cfg(any(target_os = "android", feature = "emulate-android"))]
 #[cfg(any(target_os = "android", feature = "emulate-android"))]
 mod android_ui_consts {
 mod android_ui_consts {
@@ -438,17 +437,8 @@ pub async fn make(
     node.set_property_f32(atom, Role::App, "fade_zone", MENU_FADE).unwrap();
     node.set_property_f32(atom, Role::App, "fade_zone", MENU_FADE).unwrap();
 
 
     let prop = node.get_property("items").unwrap();
     let prop = node.get_property("items").unwrap();
-    for item in channels_tree.iter() {
-        let (key, val) = item.unwrap();
-        let channel = deserialize::<Channel>(&val).unwrap();
-        let channel_name = format!("#{}", channel.name);
-        prop.push_str(atom, Role::App, &channel_name).unwrap();
-    }
-    for item in contacts_tree.iter() {
-        let (_key, val) = item.unwrap();
-        let contact = deserialize::<Contact>(&val).unwrap();
-        let contact_name = format!("@{}", contact.name);
-        prop.push_str(atom, Role::App, &contact_name).unwrap();
+    for name in read_joined_channels() {
+        prop.push_str(atom, Role::App, &name).unwrap();
     }
     }
 
 
     let (slot, recvr) = Slot::new("menu_clicked");
     let (slot, recvr) = Slot::new("menu_clicked");
@@ -478,20 +468,36 @@ pub async fn make(
         node.setup(|me| Menu::new(me, window_scale.clone(), app.renderer.clone())).await;
         node.setup(|me| Menu::new(me, window_scale.clone(), app.renderer.clone())).await;
     layer_node.link(menu_node.clone());
     layer_node.link(menu_node.clone());
 
 
-    // Subscribe to edit_done signal to log deleted items
+    // Subscribe to edit_done signal to persist the joined list and unlink removed layers
     let (edit_done_slot, edit_done_recvr) = Slot::new("edit_done");
     let (edit_done_slot, edit_done_recvr) = Slot::new("edit_done");
     menu_node.register("edit_done", edit_done_slot).unwrap();
     menu_node.register("edit_done", edit_done_slot).unwrap();
     let sg_root = app.sg_root.clone();
     let sg_root = app.sg_root.clone();
+    let menu_node2 = menu_node.clone();
     let edit_done_listen = app.ex.spawn(async move {
     let edit_done_listen = app.ex.spawn(async move {
         while let Ok(data) = edit_done_recvr.recv().await {
         while let Ok(data) = edit_done_recvr.recv().await {
             let deleted_items: Vec<String> = deserialize(&data).unwrap();
             let deleted_items: Vec<String> = deserialize(&data).unwrap();
-            for item in deleted_items {
+
+            // Persist the post-edit joined list (captures removals + reorders)
+            let current: Vec<String> =
+                menu_node2.get_property("items").unwrap().get_str_vec().unwrap();
+            write_joined_channels(&current);
+
+            for item in &deleted_items {
                 let path = format!("/window/content/{}_chat_layer", item);
                 let path = format!("/window/content/{}_chat_layer", item);
-                let node = sg_root.lookup_node(path).unwrap();
-                node.clear_tasks();
-                debug!(target: "app::menu", "deleted item: {item}");
-                node.unlink();
+                if let Some(node) = sg_root.lookup_node(&path) {
+                    node.clear_tasks();
+                    debug!(target: "app::menu", "deleted item: {item}");
+                    node.unlink();
+                }
             }
             }
+
+            // TODO: reload plugin so it drops un-joined channels/contacts from its maps.
+            // if let Some(darkirc) = sg_root.lookup_node("/plugin/darkirc") {
+            //     for item in &deleted_items {
+            //         let data = serialize(item);
+            //         let _ = darkirc.call_method("rescan", data).await;
+            //     }
+            // }
         }
         }
     });
     });
     app.tasks.lock().unwrap().push(edit_done_listen);
     app.tasks.lock().unwrap().push(edit_done_listen);

+ 82 - 27
bin/app/src/app/schema/mod.rs

@@ -20,7 +20,7 @@ use darkfi::system::msleep;
 use darkfi_serial::{deserialize, Encodable};
 use darkfi_serial::{deserialize, Encodable};
 use indoc::indoc;
 use indoc::indoc;
 use sled_overlay::sled;
 use sled_overlay::sled;
-use std::fs::File;
+use std::{fs::File, io::Write};
 
 
 use crate::{
 use crate::{
     app::{
     app::{
@@ -38,7 +38,7 @@ use crate::{
 
 
 mod chat;
 mod chat;
 pub mod menu;
 pub mod menu;
-use menu::{channel::Channel, contact::Contact};
+use menu::channel::Channel;
 //mod settings;
 //mod settings;
 pub mod test;
 pub mod test;
 pub mod test_edit;
 pub mod test_edit;
@@ -83,6 +83,10 @@ mod ui_consts {
     pub fn get_main_db_path() -> PathBuf {
     pub fn get_main_db_path() -> PathBuf {
         get_appdata_path().join("db")
         get_appdata_path().join("db")
     }
     }
+
+    pub fn get_joined_channels_filename() -> PathBuf {
+        get_appdata_path().join("joined.txt")
+    }
 }
 }
 
 
 #[cfg(not(target_os = "android"))]
 #[cfg(not(target_os = "android"))]
@@ -107,6 +111,10 @@ mod desktop_paths {
     pub fn get_main_db_path() -> PathBuf {
     pub fn get_main_db_path() -> PathBuf {
         dirs::data_local_dir().unwrap().join("darkfi/app/db")
         dirs::data_local_dir().unwrap().join("darkfi/app/db")
     }
     }
+
+    pub fn get_joined_channels_filename() -> PathBuf {
+        dirs::cache_dir().unwrap().join("darkfi/app/joined.txt")
+    }
 }
 }
 
 
 #[cfg(feature = "emulate-android")]
 #[cfg(feature = "emulate-android")]
@@ -137,6 +145,62 @@ enum ColorScheme {
     PaperLight,
     PaperLight,
 }
 }
 
 
+/// Read the ordered list of joined channels/contacts (prefixed names like "#dev", "@alice").
+pub fn read_joined_channels() -> Vec<String> {
+    let Ok(contents) = std::fs::read_to_string(get_joined_channels_filename()) else {
+        return vec![]
+    };
+
+    let mut joined = vec![];
+    for line in contents.lines() {
+        let line = line.trim();
+        assert!(!line.is_empty());
+        joined.push(line.to_string());
+    }
+    joined
+}
+
+/// First-run seed: write DEFAULT_CHANNELS (as "#name") if no joined file exists.
+/// Idempotent and deterministic, so it is safe to call from both schema startup and the plugin.
+pub fn ensure_joined_channels_seeded() {
+    let path = get_joined_channels_filename();
+    if path.exists() {
+        return
+    }
+    if let Some(parent) = path.parent() {
+        let _ = std::fs::create_dir_all(parent);
+    }
+    let defaults: Vec<String> = DEFAULT_CHANNELS.iter().map(|c| format!("#{}", c)).collect();
+    let _ = std::fs::write(&path, defaults.join("\n"));
+}
+
+/// Append a single channel line to the joined file (no dedup). Ensures newline separation
+/// when appending to a non-empty file that lacks a trailing newline.
+pub fn write_joined_channel(name: &str) {
+    let path = get_joined_channels_filename();
+    let needs_newline = std::fs::read(&path)
+        .map(|bytes| !bytes.is_empty() && !bytes.ends_with(b"\n"))
+        .unwrap_or(false);
+    let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&path).unwrap();
+    if needs_newline {
+        writeln!(f).unwrap();
+    }
+    writeln!(f, "{}", name).unwrap();
+}
+
+/// Join a channel: skip if already joined, otherwise append one line.
+pub fn append_joined_channel(name: &str) {
+    if read_joined_channels().iter().any(|x| x == name) {
+        return
+    }
+    write_joined_channel(name);
+}
+
+/// Rewrite the whole joined list (used for first-run seed and main-menu edit_done sync).
+pub fn write_joined_channels(items: &[String]) {
+    let _ = std::fs::write(get_joined_channels_filename(), items.join("\n"));
+}
+
 pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db: sled::Db) {
 pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db: sled::Db) {
     let mut cc = Compiler::new();
     let mut cc = Compiler::new();
     cc.add_const_f32("NETSTATUS_ICON_SIZE", NETSTATUS_ICON_SIZE);
     cc.add_const_f32("NETSTATUS_ICON_SIZE", NETSTATUS_ICON_SIZE);
@@ -613,37 +677,28 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
         let _ = channels_tree.flush_async().await;
         let _ = channels_tree.flush_async().await;
     }
     }
 
 
-    // Initialize chat layers from channels in database
-    for item in channels_tree.iter() {
-        let (key, val) = item.unwrap();
-        let channel = deserialize::<Channel>(&val).unwrap();
-        let channel_name = format!("#{}", channel.name);
-
-        chat::make(
-            &app.sg_root,
-            &app.renderer,
-            &app.ex,
-            content.clone(),
-            &channel_name,
-            &db,
-            i18n_fish,
-            emoji_meshes.clone(),
-            is_first_time,
-        )
-        .await;
-    }
-    // Initialize chat layers from contacts in database
-    for item in contacts_tree.iter() {
-        let (_key, val) = item.unwrap();
-        let contact = deserialize::<Contact>(&val).unwrap();
-        let contact_name = format!("@{}", contact.name);
+    // Seed the joined-channels file with defaults on first run.
+    ensure_joined_channels_seeded();
+
+    // Create chat layers only for joined channels/contacts, in joined order.
+    for name in read_joined_channels() {
+        let bare = if name.starts_with('#') || name.starts_with('@') { &name[1..] } else { &name };
+        let in_tree = match name.chars().next() {
+            Some('#') => channels_tree.contains_key(bare).unwrap_or(false),
+            Some('@') => contacts_tree.contains_key(bare).unwrap_or(false),
+            _ => false,
+        };
+        if !in_tree {
+            warn!(target: "app::schema", "Joined entry '{name}' not found in tree; skipping");
+            continue
+        }
 
 
         chat::make(
         chat::make(
             &app.sg_root,
             &app.sg_root,
             &app.renderer,
             &app.renderer,
             &app.ex,
             &app.ex,
             content.clone(),
             content.clone(),
-            &contact_name,
+            &name,
             &db,
             &db,
             i18n_fish,
             i18n_fish,
             emoji_meshes.clone(),
             emoji_meshes.clone(),

+ 22 - 2
bin/app/src/plugin/darkirc.rs

@@ -52,7 +52,11 @@ use parking_lot::Mutex as SyncMutex;
 use sled_overlay::sled;
 use sled_overlay::sled;
 
 
 use crate::{
 use crate::{
-    app::schema::menu::{channel::Channel, contact::Contact},
+    app::schema::{
+        ensure_joined_channels_seeded,
+        menu::{channel::Channel, contact::Contact},
+        read_joined_channels,
+    },
     error::{Error, Result},
     error::{Error, Result},
     prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyPtr, PropertyStr, Role},
     prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyPtr, PropertyStr, Role},
     scene::{MethodCallSub, Pimpl, SceneNode, SceneNodePtr, SceneNodeType, SceneNodeWeak, Slot},
     scene::{MethodCallSub, Pimpl, SceneNode, SceneNodePtr, SceneNodeType, SceneNodeWeak, Slot},
@@ -340,6 +344,7 @@ impl DarkIrc {
             ex: ex.clone(),
             ex: ex.clone(),
         });
         });
 
 
+        ensure_joined_channels_seeded();
         self_.load_channels_from_db().await;
         self_.load_channels_from_db().await;
         self_.load_contacts_from_db().await;
         self_.load_contacts_from_db().await;
         self_.clone().start(sg_root, ex).await;
         self_.clone().start(sg_root, ex).await;
@@ -645,14 +650,22 @@ impl DarkIrc {
     /// Load channels from UI database and populate encryption keys
     /// Load channels from UI database and populate encryption keys
     pub async fn load_channels_from_db(&self) {
     pub async fn load_channels_from_db(&self) {
         let mut channels = self.channels.write().await;
         let mut channels = self.channels.write().await;
+        channels.clear();
+
+        let joined: HashSet<String> = read_joined_channels().into_iter().collect();
 
 
         for item in self.channels_tree.iter() {
         for item in self.channels_tree.iter() {
             let (key, val) = item.unwrap();
             let (key, val) = item.unwrap();
             let channel_name = String::from_utf8_lossy(&key).to_string();
             let channel_name = String::from_utf8_lossy(&key).to_string();
+            let full_name = format!("#{}", channel_name);
+
+            if !joined.contains(&full_name) {
+                continue
+            }
+
             let ui_channel = deserialize_async::<Channel>(&val).await.unwrap();
             let ui_channel = deserialize_async::<Channel>(&val).await.unwrap();
 
 
             // Convert to IrcChannel with encryption
             // Convert to IrcChannel with encryption
-            let full_name = format!("#{}", channel_name);
             let mut irc_channel =
             let mut irc_channel =
                 IrcChannel { topic: String::new(), nicks: HashSet::new(), saltbox: None };
                 IrcChannel { topic: String::new(), nicks: HashSet::new(), saltbox: None };
 
 
@@ -693,9 +706,16 @@ impl DarkIrc {
         let mut contacts = self.contacts.write().await;
         let mut contacts = self.contacts.write().await;
         contacts.clear();
         contacts.clear();
 
 
+        let joined: HashSet<String> = read_joined_channels().into_iter().collect();
+
         for item in self.contacts_tree.iter() {
         for item in self.contacts_tree.iter() {
             let (key, val) = item.unwrap();
             let (key, val) = item.unwrap();
             let name = String::from_utf8_lossy(&key).to_string();
             let name = String::from_utf8_lossy(&key).to_string();
+
+            if !joined.contains(&format!("@{}", name)) {
+                continue
+            }
+
             let contact = deserialize_async::<Contact>(&val).await.unwrap();
             let contact = deserialize_async::<Contact>(&val).await.unwrap();
 
 
             let their_public = PublicKey::from(contact.public);
             let their_public = PublicKey::from(contact.public);