diff --git a/Cargo.lock b/Cargo.lock index 5b5a4668..938ce6e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,8 @@ version = "0.1.0" dependencies = [ "aardvark-doc", "aardvark-node", + "ashpd", + "futures-util", "gettext-rs", "gtk4", "libadwaita", @@ -23,6 +25,7 @@ dependencies = [ "aardvark-node", "anyhow", "async-channel", + "gio", "glib", "loro", "p2panda-core", @@ -158,6 +161,25 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ccd462b64c3c72f1be8305905a85d85403d768e8690c9b8bd3b9009a5761679" +[[package]] +name = "ashpd" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d43c03d9e36dd40cab48435be0b09646da362c278223ca535493877b2c1dee9" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "serde", + "serde_repr", + "tracing", + "url", + "zbus", +] + [[package]] name = "asn1-rs" version = "0.6.2" @@ -197,6 +219,18 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "2.3.1" @@ -209,6 +243,90 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 0.38.44", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 0.38.44", + "tracing", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -220,6 +338,30 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.44", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.88" @@ -364,6 +506,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bounded-integer" version = "0.5.8" @@ -934,6 +1089,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "ensure-cov" version = "0.1.0" @@ -983,6 +1144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" dependencies = [ "enumflags2_derive", + "serde", ] [[package]] @@ -1017,6 +1179,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a02a5d186d7bf1cb21f1f95e1a9cfa5c1f2dcd803a47aad454423ceec13525c5" +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "event-listener" version = "5.4.0" @@ -1700,6 +1872,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -2557,6 +2735,18 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + [[package]] name = "litemap" version = "0.7.5" @@ -3077,6 +3267,19 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "no-std-net" version = "0.6.0" @@ -3274,6 +3477,16 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "overload" version = "0.1.1" @@ -3534,6 +3747,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkarr" version = "2.3.1" @@ -3616,6 +3840,21 @@ dependencies = [ "pnet_macros_support", ] +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.44", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -4122,6 +4361,32 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.3", + "windows-sys 0.59.0", +] + [[package]] name = "rustls" version = "0.23.23" @@ -4372,6 +4637,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -4768,6 +5044,19 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc1ee6eef34f12f765cb94725905c6312b6610ab2b0940889cfe58dae7bc3c72" +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix 1.0.3", + "windows-sys 0.59.0", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -5160,6 +5449,17 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -5984,6 +6284,16 @@ dependencies = [ "time", ] +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "xml-rs" version = "0.8.25" @@ -6050,6 +6360,68 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -6138,3 +6510,41 @@ dependencies = [ "quote", "syn 2.0.100", ] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] diff --git a/aardvark-app/Cargo.toml b/aardvark-app/Cargo.toml index e988e559..9cdfe104 100644 --- a/aardvark-app/Cargo.toml +++ b/aardvark-app/Cargo.toml @@ -16,6 +16,8 @@ gtk = { version = "0.9", package = "gtk4", features = ["gnome_47"] } sourceview = { package = "sourceview5", version = "0.9" } tracing = "0.1" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +ashpd = { version = "0.9", default-features = false, features = ["tracing", "async-std"] } +futures-util = "0.3" [dependencies.adw] package = "libadwaita" diff --git a/aardvark-app/src/application.rs b/aardvark-app/src/application.rs index 57e24394..59f243af 100644 --- a/aardvark-app/src/application.rs +++ b/aardvark-app/src/application.rs @@ -26,6 +26,7 @@ use gtk::{gio, glib, glib::Properties}; use crate::AardvarkWindow; use crate::config; +use crate::system_settings::SystemSettings; mod imp { use super::*; @@ -35,6 +36,8 @@ mod imp { pub struct AardvarkApplication { #[property(get)] pub service: Service, + #[property(get)] + pub system_settings: SystemSettings, } #[glib::object_subclass] @@ -149,3 +152,11 @@ impl AardvarkApplication { about.present(Some(&window)); } } + +impl Default for AardvarkApplication { + fn default() -> Self { + gio::Application::default() + .and_downcast::() + .unwrap() + } +} diff --git a/aardvark-app/src/components/avatar.rs b/aardvark-app/src/components/avatar.rs new file mode 100644 index 00000000..d9597f6a --- /dev/null +++ b/aardvark-app/src/components/avatar.rs @@ -0,0 +1,55 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gtk::{glib, glib::GString}; + +mod imp { + use super::*; + + #[derive(Default, glib::Properties)] + #[properties(wrapper_type = super::Avatar)] + pub struct Avatar { + #[property(name = "emoji", get = Self::emoji, set = Self::set_emoji, type = GString)] + label: gtk::Label, + } + + #[glib::object_subclass] + impl ObjectSubclass for Avatar { + const NAME: &'static str = "Avatar"; + type Type = super::Avatar; + type ParentType = adw::Bin; + } + + #[glib::derived_properties] + impl ObjectImpl for Avatar { + fn constructed(&self) { + self.parent_constructed(); + self.obj().set_child(Some(&self.label)); + self.obj().add_css_class("avatar"); + self.obj().set_valign(gtk::Align::Center); + self.obj().set_halign(gtk::Align::Center); + } + } + + impl Avatar { + fn emoji(&self) -> GString { + self.label.label() + } + + fn set_emoji(&self, emoji: &str) { + self.label.set_label(emoji); + } + } + + impl WidgetImpl for Avatar {} + impl BinImpl for Avatar {} +} + +glib::wrapper! { + pub struct Avatar(ObjectSubclass) + @extends gtk::Widget, adw::Bin; +} + +impl Avatar { + pub fn new() -> Self { + glib::Object::new() + } +} diff --git a/aardvark-app/src/components/mod.rs b/aardvark-app/src/components/mod.rs index 24b45d70..905a7eaf 100644 --- a/aardvark-app/src/components/mod.rs +++ b/aardvark-app/src/components/mod.rs @@ -1,5 +1,7 @@ +mod avatar; mod multiline_entry; mod zoom_level_selector; +pub use self::avatar::Avatar; pub use self::multiline_entry::MultilineEntry; pub use self::zoom_level_selector::ZoomLevelSelector; diff --git a/aardvark-app/src/connection_popover/mod.rs b/aardvark-app/src/connection_popover/mod.rs new file mode 100644 index 00000000..b291bfb0 --- /dev/null +++ b/aardvark-app/src/connection_popover/mod.rs @@ -0,0 +1,265 @@ +/* window.rs + * + * Copyright 2024 The Aardvark Developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +use std::cell::RefCell; + +use adw::prelude::ActionRowExt; +use adw::subclass::prelude::*; +use gettextrs::gettext; +use gtk::glib; +use gtk::prelude::*; + +use crate::AardvarkApplication; +use crate::components::Avatar; +use crate::system_settings::ClockFormat; +use aardvark_doc::{author::Author, author::COLORS, authors::Authors}; + +mod imp { + use super::*; + + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::ConnectionPopover)] + pub struct ConnectionPopover { + author_list_box: gtk::ListBox, + #[property(get, set = Self::set_model)] + model: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for ConnectionPopover { + const NAME: &'static str = "AardvarkConnectionPopover"; + type Type = super::ConnectionPopover; + type ParentType = gtk::Popover; + } + + #[glib::derived_properties] + impl ObjectImpl for ConnectionPopover { + fn constructed(&self) { + let scrollview = gtk::ScrolledWindow::builder() + .child(&self.author_list_box) + .hscrollbar_policy(gtk::PolicyType::Never) + .propagate_natural_height(true) + .propagate_natural_width(true) + .max_content_height(300) + .build(); + self.obj().set_child(Some(&scrollview)); + self.author_list_box + .set_selection_mode(gtk::SelectionMode::None); + self.obj().add_css_class("connection-popover"); + + let css_provider = gtk::CssProvider::new(); + let style: String = COLORS + .iter() + .map(|(color_name, color_hex)| { + format!(".bg-{color_name} {{ background-color: {color_hex}; }}") + }) + .collect(); + css_provider.load_from_string(&style); + gtk::style_context_add_provider_for_display( + &self.obj().display(), + &css_provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + } + } + + impl ConnectionPopover { + fn set_model(&self, model: Option) { + self.author_list_box.bind_model(model.as_ref(), |author| { + let author = author.downcast_ref::().unwrap(); + let row = adw::ActionRow::builder() + .selectable(false) + .activatable(false) + .can_focus(false) + .can_target(false) + .build(); + let avatar = Avatar::new(); + row.add_prefix(&avatar); + if author.is_this_device() { + let this_device_label = gtk::Label::builder() + .label("This Device") + .valign(gtk::Align::Start) + .margin_top(6) + .css_classes(["this-device-pill"]) + .build(); + row.add_suffix(&this_device_label); + } + author + .bind_property("name", &row, "title") + .sync_create() + .build(); + // FIXME: format last seen according to the mockups + //author.bind_property ("last-seen", row, "subtitle").sync_create().build(); + author + .bind_property("emoji", &avatar, "emoji") + .sync_create() + .build(); + author + .bind_property("is-online", &row, "subtitle") + .sync_create() + .transform_to(|binding, is_online: bool| { + let author: Author = binding.source().unwrap().downcast().unwrap(); + if is_online { + Some("Online".to_string()) + //Some(format_last_seen(&glib::DateTime::now_local().unwrap())) + } else { + Some(format_last_seen(&author.last_seen().unwrap())) + } + }) + .build(); + avatar.add_css_class(&format!("bg-{}", author.color())); + + row.upcast() + }); + + self.model.replace(model); + } + } + + impl WidgetImpl for ConnectionPopover {} + impl PopoverImpl for ConnectionPopover {} +} + +glib::wrapper! { + pub struct ConnectionPopover(ObjectSubclass) + @extends gtk::Widget, gtk::Popover; +} + +impl ConnectionPopover { + pub fn new>(model: &P) -> Self { + glib::Object::builder().property("model", model).build() + } +} + +// This was copied from Fractal +// See: https://gitlab.gnome.org/World/fractal/-/blob/main/src/session/model/user_sessions_list/user_session.rs#L258 +fn format_last_seen(datetime: &glib::DateTime) -> String { + let clock_format = AardvarkApplication::default() + .system_settings() + .clock_format(); + let use_24 = clock_format == ClockFormat::TwentyFourHours; + + // This was ported from Nautilus and simplified for our use case. + // See: https://gitlab.gnome.org/GNOME/nautilus/-/blob/1c5bd3614a35cfbb49de087bc10381cdef5a218f/src/nautilus-file.c#L5001 + let now = glib::DateTime::now_local().unwrap(); + let format; + let days_ago = { + let today_midnight = + glib::DateTime::from_local(now.year(), now.month(), now.day_of_month(), 0, 0, 0f64) + .expect("constructing GDateTime works"); + + let date = glib::DateTime::from_local( + datetime.year(), + datetime.month(), + datetime.day_of_month(), + 0, + 0, + 0f64, + ) + .expect("constructing GDateTime works"); + + today_midnight.difference(&date).as_days() + }; + + // Show only the time if date is on today + if days_ago == 0 { + if use_24 { + // Translators: Time in 24h format, i.e. "23:04". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + format = gettext("Last seen at %H:%M"); + } else { + // Translators: Time in 12h format, i.e. "11:04 PM". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + format = gettext("Last seen at %I:%M %p"); + } + } + // Show the word "Yesterday" and time if date is on yesterday + else if days_ago == 1 { + if use_24 { + // Translators: this a time in 24h format, i.e. "Last seen yesterday at 23:04". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen yesterday at %H:%M"); + } else { + // Translators: this is a time in 12h format, i.e. "Last seen Yesterday at 11:04 + // PM". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen yesterday at %I:%M %p"); + } + } + // Show a week day and time if date is in the last week + else if days_ago > 1 && days_ago < 7 { + if use_24 { + // Translators: this is the name of the week day followed by a time in 24h + // format, i.e. "Last seen Monday at 23:04". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %A at %H:%M"); + } else { + // Translators: this is the week day name followed by a time in 12h format, i.e. + // "Last seen Monday at 11:04 PM". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %A at %I:%M %p"); + } + } else if datetime.year() == now.year() { + if use_24 { + // Translators: this is the month and day and the time in 24h format, i.e. "Last + // seen February 3 at 23:04". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %B %-e at %H:%M"); + } else { + // Translators: this is the month and day and the time in 12h format, i.e. "Last + // seen February 3 at 11:04 PM". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %B %-e at %I:%M %p"); + } + } else if use_24 { + // Translators: this is the full date and the time in 24h format, i.e. "Last + // seen February 3 2015 at 23:04". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %B %-e %Y at %H:%M"); + } else { + // Translators: this is the full date and the time in 12h format, i.e. "Last + // seen February 3 2015 at 11:04 PM". + // Do not change the time format as it will follow the system settings. + // See `man strftime` or the documentation of g_date_time_format for the available specifiers: + // xgettext:no-c-format + format = gettext("Last seen %B %-e %Y at %I:%M %p"); + } + + datetime + .format(&format) + .expect("formatting GDateTime works") + .into() +} diff --git a/aardvark-app/src/main.rs b/aardvark-app/src/main.rs index aa005edc..a142b45c 100644 --- a/aardvark-app/src/main.rs +++ b/aardvark-app/src/main.rs @@ -21,6 +21,8 @@ mod application; mod components; mod config; +mod connection_popover; +mod system_settings; mod textbuffer; mod window; @@ -34,6 +36,7 @@ use tracing_subscriber::prelude::*; use self::application::AardvarkApplication; use self::config::{GETTEXT_PACKAGE, LOCALEDIR, PKGDATADIR}; +use self::connection_popover::ConnectionPopover; use self::textbuffer::AardvarkTextBuffer; use self::window::AardvarkWindow; diff --git a/aardvark-app/src/store.rs b/aardvark-app/src/store.rs new file mode 100644 index 00000000..e69de29b diff --git a/aardvark-app/src/style.css b/aardvark-app/src/style.css index 61dfb421..8da892e9 100644 --- a/aardvark-app/src/style.css +++ b/aardvark-app/src/style.css @@ -15,3 +15,27 @@ .user-counter { font-weight: bold; } + +.connection-popover > contents { + padding: 0px; +} + +.avatar { + min-width: 46px; + min-height: 46px; + border-radius: 32px; + font-size: 32px; +} + +.connection-popover list { + margin: 9px 3px; +} + +.this-device-pill { + border-radius: 9px; + padding: 3px 6px; + background-color: color-mix(in srgb, var(--accent-bg-color) 25%, transparent); + color: var(--accent-color); + font-weight: 700; + font-size: 9px; +} diff --git a/aardvark-app/src/system_settings.rs b/aardvark-app/src/system_settings.rs new file mode 100644 index 00000000..9e79f5dc --- /dev/null +++ b/aardvark-app/src/system_settings.rs @@ -0,0 +1,238 @@ +/* system_settings.rs + * + * Copyright 2025 The Aardvark Developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +use adw::prelude::*; +use adw::subclass::prelude::*; +use ashpd::{desktop::settings::Settings as SettingsProxy, zvariant}; +use futures_util::stream::StreamExt; +use gtk::{glib, glib::Properties, glib::clone, pango}; +use std::cell::{Cell, RefCell}; +use tracing::error; + +const GNOME_DESKTOP_NAMESPACE: &str = "org.gnome.desktop.interface"; +const CLOCK_FORMAT_KEY: &str = "clock-format"; +const MONOSPACE_FONT_NAME_KEY: &str = "monospace-font-name"; + +/// The clock format setting. +#[derive(Debug, Clone, Copy, PartialEq, Eq, glib::Enum)] +#[repr(u32)] +#[enum_type(name = "ClockFormat")] +pub enum ClockFormat { + /// The 12h format, i.e. AM/PM. + TwelveHours = 0, + /// The 24h format. + TwentyFourHours = 1, +} + +impl Default for ClockFormat { + fn default() -> Self { + // Use the locale's default clock format as a fallback. + let local_formatted_time = glib::DateTime::now_local() + .and_then(|d| d.format("%X")) + .map(|s| s.to_ascii_lowercase()); + match &local_formatted_time { + Ok(s) if s.ends_with("am") || s.ends_with("pm") => ClockFormat::TwelveHours, + Ok(_) => ClockFormat::TwentyFourHours, + Err(error) => { + error!("Could not get local formatted time: {error}"); + ClockFormat::TwelveHours + } + } + } +} + +mod imp { + use super::*; + + #[derive(Properties, Default)] + #[properties(wrapper_type = super::SystemSettings)] + pub struct SystemSettings { + /// The clock format setting. + #[property(get, builder(ClockFormat::default()))] + pub clock_format: Cell, + // The monospace font name setting. + #[property(get)] + pub monospace_font_name: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for SystemSettings { + const NAME: &'static str = "SystemSettings"; + type Type = super::SystemSettings; + type ParentType = glib::Object; + } + + #[glib::derived_properties] + impl ObjectImpl for SystemSettings { + fn constructed(&self) { + self.parent_constructed(); + + glib::spawn_future_local(clone!( + #[weak(rename_to = this)] + self, + async move { + if let Err(error) = this.init().await { + error!("Unable to read system settings: {error}"); + } + } + )); + } + } + + impl SystemSettings { + async fn init(&self) -> Result<(), ashpd::Error> { + let proxy = SettingsProxy::new().await?; + let settings = proxy.read_all(&[GNOME_DESKTOP_NAMESPACE]).await?; + + if let Some(namespace) = settings.get(GNOME_DESKTOP_NAMESPACE) { + if let Some(clock_format) = namespace.get(CLOCK_FORMAT_KEY) { + match ClockFormat::try_from(clock_format) { + Ok(clock_format) => { + self.set_clock_format(clock_format); + } + Err(error) => { + error!("Unable to read clock format system setting: {error}"); + self.set_clock_format(ClockFormat::default()); + } + }; + } + if let Some(font_name) = namespace.get(MONOSPACE_FONT_NAME_KEY) { + match <&str>::try_from(font_name) + .and_then(|font_name| Ok(pango::FontDescription::from_string(font_name))) + { + Ok(font) => { + self.set_monospace_font_name(Some(font)); + } + Err(error) => { + error!("Unable to read monofont system setting: {error}"); + self.set_monospace_font_name(None); + } + }; + } + } + + let setting_changed_stream = proxy.receive_setting_changed().await?; + + let obj_weak = self.obj().downgrade(); + setting_changed_stream + .for_each(move |setting| { + let obj_weak = obj_weak.clone(); + async move { + if setting.namespace() != GNOME_DESKTOP_NAMESPACE { + return; + } + + if let Some(obj) = obj_weak.upgrade() { + if setting.key() == CLOCK_FORMAT_KEY { + match ClockFormat::try_from(setting.value()) { + Ok(clock_format) => { + obj.imp().set_clock_format(clock_format); + } + Err(error) => { + error!( + "Unable to read clock format system setting: {error}" + ); + obj.imp().set_clock_format(ClockFormat::default()); + } + }; + } + + if setting.key() == MONOSPACE_FONT_NAME_KEY { + match <&str>::try_from(setting.value()).and_then(|font_name| { + Ok(pango::FontDescription::from_string(font_name)) + }) { + Ok(font) => { + obj.imp().set_monospace_font_name(Some(font)); + } + Err(error) => { + error!("Unable to read monofont system setting: {error}"); + obj.imp().set_monospace_font_name(None); + } + }; + } + } + } + }) + .await; + + Ok(()) + } + + fn set_clock_format(&self, clock_format: ClockFormat) { + if self.obj().clock_format() == clock_format { + return; + } + + self.clock_format.set(clock_format); + self.obj().notify_clock_format(); + } + + fn set_monospace_font_name(&self, font_name: Option) { + if self.obj().monospace_font_name() == font_name { + return; + } + + self.monospace_font_name.replace(font_name); + self.obj().notify_monospace_font_name(); + } + } +} + +glib::wrapper! { + pub struct SystemSettings(ObjectSubclass); +} + +impl SystemSettings { + pub fn new() -> Self { + glib::Object::new() + } +} + +impl Default for SystemSettings { + fn default() -> Self { + Self::new() + } +} + +impl TryFrom<&zvariant::OwnedValue> for ClockFormat { + type Error = zvariant::Error; + + fn try_from(value: &zvariant::OwnedValue) -> Result { + let Ok(s) = <&str>::try_from(value) else { + return Err(zvariant::Error::IncorrectType); + }; + + match s { + "12h" => Ok(Self::TwelveHours), + "24h" => Ok(Self::TwentyFourHours), + _ => Err(zvariant::Error::Message(format!( + "Invalid string `{s}`, expected `12h` or `24h`" + ))), + } + } +} + +impl TryFrom for ClockFormat { + type Error = zvariant::Error; + + fn try_from(value: zvariant::OwnedValue) -> Result { + Self::try_from(&value) + } +} diff --git a/aardvark-app/src/window.rs b/aardvark-app/src/window.rs index cc87b3c4..390fb3ce 100644 --- a/aardvark-app/src/window.rs +++ b/aardvark-app/src/window.rs @@ -33,7 +33,7 @@ use gtk::{gdk, gio, glib, glib::clone}; use sourceview::*; use crate::{ - AardvarkApplication, AardvarkTextBuffer, + AardvarkApplication, AardvarkTextBuffer, ConnectionPopover, components::{MultilineEntry, ZoomLevelSelector}, }; @@ -65,6 +65,10 @@ mod imp { pub copy_code_button: TemplateChild, #[template_child] pub open_document_entry: TemplateChild, + #[template_child] + pub connection_button: TemplateChild, + #[template_child] + pub connection_button_label: TemplateChild, pub css_provider: gtk::CssProvider, pub font_size: Cell, #[property(get, set = Self::set_font_scale, default = 0.0)] @@ -367,6 +371,21 @@ mod imp { .downcast::() .unwrap() .set_document(&document); + let authors = document.authors(); + self.connection_button + .set_popover(Some(&ConnectionPopover::new(&authors))); + // TODO: we need to do the same as fractal to allow gettext string substitution + //self.connection_button.set_tooltip_text(gettext!("{} People Connected", authors.n_items())); + authors.connect_items_changed(clone!( + #[weak(rename_to = this)] + self, + move |authors, _, _, _| { + this.connection_button_label + .set_label(&format!("{}", authors.n_items())); + } + )); + self.connection_button_label + .set_label(&format!("{}", authors.n_items())); self.document.replace(Some(document)); self.obj().notify("document"); diff --git a/aardvark-app/src/window.ui b/aardvark-app/src/window.ui index 3a3d71a8..d860d458 100644 --- a/aardvark-app/src/window.ui +++ b/aardvark-app/src/window.ui @@ -75,22 +75,28 @@ - + 6 + - - 6 - system-users-symbolic - 34 People Connected - - - - - 34 - False - + + + + 6 + system-users-symbolic + + + + + 34 + False + + + diff --git a/aardvark-doc/Cargo.toml b/aardvark-doc/Cargo.toml index e6f263ae..98ea4235 100644 --- a/aardvark-doc/Cargo.toml +++ b/aardvark-doc/Cargo.toml @@ -12,8 +12,9 @@ aardvark-node = { path = "../aardvark-node" } anyhow = "1.0.94" async-channel = "2.3.1" glib = "0.20" +gio = "0.20" loro = "1.3.1" p2panda-core = { git = "https://github.com/p2panda/p2panda", rev = "f3a016324b69beac45cf20a792fe6890cb1a21e3", default-features = false } thiserror = "2.0.11" tokio = { version = "1.42.0", features = ["macros", "test-util"] } -tracing = "0.1" +tracing = "0.1" \ No newline at end of file diff --git a/aardvark-doc/src/author.rs b/aardvark-doc/src/author.rs new file mode 100644 index 00000000..0a19a104 --- /dev/null +++ b/aardvark-doc/src/author.rs @@ -0,0 +1,165 @@ +use std::cell::{Cell, OnceCell}; +use std::sync::Mutex; + +use glib::Properties; +use glib::prelude::*; +use glib::subclass::prelude::*; +use p2panda_core::PublicKey; + +pub const COLORS: [(&str, &str); 15] = [ + ("Yellow", "#faf387"), + ("Orange", "#ffc885"), + ("Red", "#f99085"), + ("Pink", "#fcaed5"), + ("Purple", "#f39bf2"), + ("Violet", "#b797f3"), + ("Blue", "#99c1f1"), + ("Cyan", "#99f1ec"), + ("Green", "#97f1aa"), + ("Khaki", "#f6e7c0"), + ("Brown", "#d9c0ab"), + ("Silver", "#deddda"), + ("Gray", "#c0bfbc"), + ("Black", "#9a9996"), + ("Gold", "#ead688"), +]; + +pub const EMOJIS: [(&str, &str); 41] = [ + ("đŸĩ", "Monkey"), + ("đŸļ", "Dog"), + ("🐱", "Cat"), + ("đŸĻŠ", "Fox"), + ("đŸē", "Wolf"), + ("đŸĻ", "Raccoon"), + ("đŸĻ", "Lion"), + ("đŸ¯", "Tiger"), + ("🐷", "Pig"), + ("🐴", "Horse"), + ("đŸĻ„", "Unicorn"), + ("đŸĻ“", "Zebra"), + ("đŸĢŽ", "Moose"), + ("🐔", "Chicken"), + ("đŸŧ", "Panda"), + ("đŸģ", "Bear"), + ("đŸģâ€â„ī¸", "Polar Bear"), + ("🐨", "Koala"), + ("🐸", "Frog"), + ("🐹", "Hamster"), + ("🐰", "Rabbit"), + ("🐮", "Cow"), + ("🐝", "Bee"), + ("đŸĸ", "Turtle"), + ("🐏", "Ram"), + ("đŸŗ", "Whale"), + ("🐙", "Octopus"), + ("đŸĻ€", "Crab"), + ("🐌", "Snail"), + ("đŸĒ˛", "Beetle"), + ("🐞", "Ladybug"), + ("đŸĻˆ", "Shark"), + ("đŸĻ­", "Seal"), + ("🐟", "Fish"), + ("đŸĻ†", "Duck"), + ("đŸĻĨ", "Sloth"), + ("đŸĻĢ", "Beaver"), + ("đŸĒ", "Camel"), + ("đŸĻ", "Gorilla"), + ("đŸĻŖ", "Mammooth"), + ("🐃", "Buffalo"), +]; + +mod imp { + use super::*; + + #[derive(Properties, Default)] + #[properties(wrapper_type = super::Author)] + pub struct Author { + #[property(name = "name", get = Self::name, type = String)] + #[property(name = "emoji", get = Self::emoji, type = String)] + #[property(name = "color", get = Self::color, type = String)] + pub public_key: OnceCell, + #[property(get)] + pub last_seen: Mutex>, + #[property(get, default = true)] + pub is_online: Cell, + #[property(get)] + pub is_this_device: Cell, + } + + #[glib::object_subclass] + impl ObjectSubclass for Author { + const NAME: &'static str = "Author"; + type Type = super::Author; + } + + #[glib::derived_properties] + impl ObjectImpl for Author {} + + impl Author { + fn name(&self) -> String { + let bytes = self.public_key.get().unwrap().as_bytes(); + let selector_color = bytes[..(bytes.len() / 2)] + .iter() + .fold(0u8, |acc, b| acc ^ b) as usize + % COLORS.len(); + let selector_emoji = bytes[(bytes.len() / 2)..] + .iter() + .fold(0u8, |acc, b| acc ^ b) as usize + % EMOJIS.len(); + format!("{} {}", COLORS[selector_color].0, EMOJIS[selector_emoji].1) + } + + fn emoji(&self) -> String { + let bytes = self.public_key.get().unwrap().as_bytes(); + let selector_emoji = bytes[(bytes.len() / 2)..] + .iter() + .fold(0u8, |acc, b| acc ^ b) as usize + % EMOJIS.len(); + EMOJIS[selector_emoji].0.to_string() + } + + fn color(&self) -> String { + let bytes = self.public_key.get().unwrap().as_bytes(); + let selector_color = bytes[..(bytes.len() / 2)] + .iter() + .fold(0u8, |acc, b| acc ^ b) as usize + % COLORS.len(); + COLORS[selector_color].0.to_string() + } + } +} + +glib::wrapper! { + pub struct Author(ObjectSubclass); +} +impl Author { + pub fn new(public_key: PublicKey) -> Self { + let obj: Self = glib::Object::new(); + + obj.imp().public_key.set(public_key).unwrap(); + obj.imp().is_online.set(true); + obj + } + + pub fn for_this_device(public_key: PublicKey) -> Self { + let obj = Self::new(public_key); + + obj.imp().is_this_device.set(true); + obj.imp().is_online.set(true); + obj + } + + pub(crate) fn public_key(&self) -> &PublicKey { + self.imp().public_key.get().unwrap() + } + + pub(crate) fn set_is_online(&self, is_online: bool) { + let was_online = self.imp().is_online.get(); + self.imp().is_online.set(is_online); + if !is_online && was_online { + *self.imp().last_seen.lock().unwrap() = glib::DateTime::now_local().ok(); + self.notify_last_seen(); + } + self.notify_is_online(); + } +} diff --git a/aardvark-doc/src/authors.rs b/aardvark-doc/src/authors.rs new file mode 100644 index 00000000..9f224e37 --- /dev/null +++ b/aardvark-doc/src/authors.rs @@ -0,0 +1,119 @@ +use p2panda_core::PublicKey; +use std::sync::Mutex; + +use gio::prelude::*; +use gio::subclass::prelude::ListModelImpl; +use glib::{clone, subclass::prelude::*}; + +use crate::author::Author; + +mod imp { + use super::*; + + #[derive(Default)] + pub struct Authors { + pub list: Mutex>, + } + + #[glib::object_subclass] + impl ObjectSubclass for Authors { + const NAME: &'static str = "Authors"; + type Type = super::Authors; + type Interfaces = (gio::ListModel,); + } + + impl ObjectImpl for Authors {} + + impl ListModelImpl for Authors { + fn item_type(&self) -> glib::Type { + Author::static_type() + } + + fn n_items(&self) -> u32 { + self.list.lock().unwrap().len() as u32 + } + + fn item(&self, index: u32) -> Option { + self.list + .lock() + .unwrap() + .get(index as usize) + .cloned() + .map(Cast::upcast) + } + } +} + +glib::wrapper! { + pub struct Authors(ObjectSubclass) + @implements gio::ListModel; +} + +unsafe impl Send for Authors {} +unsafe impl Sync for Authors {} + +impl Default for Authors { + fn default() -> Self { + Self::new() + } +} + +impl Authors { + pub fn new() -> Self { + glib::Object::new() + } + + pub(crate) fn add_this_device(&self, author_key: PublicKey) { + glib::source::idle_add_full( + glib::source::Priority::DEFAULT, + clone!( + #[weak(rename_to = obj)] + self, + #[upgrade_or] + glib::ControlFlow::Break, + move || { + let mut list = obj.imp().list.lock().unwrap(); + let pos = list.len() as u32; + + let author = Author::for_this_device(author_key); + list.push(author); + drop(list); + obj.items_changed(pos, 0, 1); + glib::ControlFlow::Break + } + ), + ); + } + + pub(crate) fn add_or_update(&self, author_key: PublicKey, is_online: bool) { + glib::source::idle_add_full( + glib::source::Priority::DEFAULT, + clone!( + #[weak(rename_to = obj)] + self, + #[upgrade_or] + glib::ControlFlow::Break, + move || { + let mut list = obj.imp().list.lock().unwrap(); + + if let Some(author) = list + .iter() + .find(|author| author.public_key() == &author_key) + { + author.set_is_online(is_online); + } else { + let pos = list.len() as u32; + + let author = Author::new(author_key); + + list.push(author); + drop(list); + + obj.items_changed(pos, 0, 1); + } + glib::ControlFlow::Break + } + ), + ); + } +} diff --git a/aardvark-doc/src/document.rs b/aardvark-doc/src/document.rs index 49da2f33..9bca38c4 100644 --- a/aardvark-doc/src/document.rs +++ b/aardvark-doc/src/document.rs @@ -13,6 +13,7 @@ use loro::{ExportMode, LoroDoc, event::Diff}; use p2panda_core::{HashError, PublicKey}; use tracing::error; +use crate::authors::Authors; use crate::service::Service; #[derive(Clone, Debug, PartialEq, Eq, glib::Boxed)] @@ -52,6 +53,8 @@ mod imp { ready: Cell, #[property(get, construct_only)] service: OnceCell, + #[property(get)] + authors: Authors, } #[glib::object_subclass] @@ -295,6 +298,10 @@ mod imp { } } )); + + // Add ourself to the list of authors + self.authors + .add_this_device(self.obj().service().public_key()); } } } @@ -330,4 +337,18 @@ impl SubscribableDocument for DocumentHandle { document.imp().on_remote_message(data); } } + + fn authors_joined(&self, authors: Vec) { + if let Some(document) = self.0.upgrade() { + for author in authors.into_iter() { + document.authors().add_or_update(author, true); + } + } + } + + fn author_set_online(&self, author: PublicKey, is_online: bool) { + if let Some(document) = self.0.upgrade() { + document.authors().add_or_update(author, is_online); + } + } } diff --git a/aardvark-doc/src/lib.rs b/aardvark-doc/src/lib.rs index 405cc979..f2d99786 100644 --- a/aardvark-doc/src/lib.rs +++ b/aardvark-doc/src/lib.rs @@ -1,3 +1,5 @@ +pub mod author; +pub mod authors; pub mod document; pub mod service; diff --git a/aardvark-node/src/document.rs b/aardvark-node/src/document.rs index 88870a9b..689422db 100644 --- a/aardvark-node/src/document.rs +++ b/aardvark-node/src/document.rs @@ -52,4 +52,6 @@ impl FromStr for DocumentId { pub trait SubscribableDocument: Sync + Send { fn bytes_received(&self, author: PublicKey, data: &[u8]); + fn authors_joined(&self, authors: Vec); + fn author_set_online(&self, author: PublicKey, is_online: bool); } diff --git a/aardvark-node/src/network.rs b/aardvark-node/src/network.rs index a2c745d5..9779f2bb 100644 --- a/aardvark-node/src/network.rs +++ b/aardvark-node/src/network.rs @@ -5,9 +5,9 @@ use anyhow::Result; use p2panda_core::{Hash, Operation, PrivateKey}; use p2panda_discovery::mdns::LocalDiscovery; use p2panda_net::config::GossipConfig; -use p2panda_net::{FromNetwork, NetworkBuilder, SyncConfiguration, ToNetwork}; +use p2panda_net::{FromNetwork, NetworkBuilder, SyncConfiguration, SystemEvent, ToNetwork}; use p2panda_stream::{DecodeExt, IngestExt}; -use tokio::sync::mpsc; +use tokio::sync::{broadcast, mpsc}; use tokio::task::JoinHandle; use tokio_stream::StreamExt; use tokio_stream::wrappers::ReceiverStream; @@ -57,6 +57,7 @@ impl Network { ) -> Result<( mpsc::Sender>, mpsc::Receiver>, + broadcast::Receiver>, )> { let (to_network, mut from_app) = mpsc::channel::>(128); let (to_app, from_network) = mpsc::channel(128); @@ -134,6 +135,8 @@ impl Network { Ok(()) }); - Ok((to_network, from_network)) + let events = self.network.events().await?; + + Ok((to_network, from_network, events)) } } diff --git a/aardvark-node/src/node.rs b/aardvark-node/src/node.rs index 83098355..4afa6244 100644 --- a/aardvark-node/src/node.rs +++ b/aardvark-node/src/node.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::Result; use p2panda_core::{Hash, PrivateKey}; -use p2panda_net::SyncConfiguration; +use p2panda_net::{SyncConfiguration, SystemEvent, TopicId}; use p2panda_sync::log_sync::LogSyncProtocol; use tokio::runtime::{Builder, Runtime}; use tokio::sync::OnceCell; @@ -122,6 +122,7 @@ impl Node { document: T, ) -> Result<()> { let private_key = self.inner.private_key.get().expect("private key").clone(); + let document = Arc::new(document); // Add ourselves as an author to the document store. self.inner @@ -130,7 +131,7 @@ impl Node { .await?; let inner_clone = self.inner.clone(); - let (document_tx, mut document_rx) = self + let (document_tx, mut document_rx, mut system_event) = self .inner .runtime .spawn(async move { @@ -152,6 +153,7 @@ impl Node { .await; let inner = self.inner.clone(); + let document_clone = document.clone(); self.inner.runtime.spawn(async move { // Process the operations and forward application messages to app layer. This is where // we "materialize" our application state from incoming "application events". @@ -175,11 +177,34 @@ impl Node { // Forward the payload up to the app. if let Some(body) = operation.body { - document.bytes_received(operation.header.public_key, &body.to_bytes()); + document_clone.bytes_received(operation.header.public_key, &body.to_bytes()); } } }); + self.inner.runtime.spawn(async move { + while let Ok(system_event) = system_event.recv().await { + match system_event { + SystemEvent::GossipJoined { topic_id, peers } + if topic_id == document_id.id() => + { + document.authors_joined(peers); + } + SystemEvent::GossipNeighborUp { topic_id, peer } + if topic_id == document_id.id() => + { + document.author_set_online(peer, true); + } + SystemEvent::GossipNeighborDown { topic_id, peer } + if topic_id == document_id.id() => + { + document.author_set_online(peer, false); + } + _ => {} + }; + } + }); + Ok(()) }