app: add fud plugin, add FileMessage to MessageBuffer

This commit is contained in:
epiphany
2025-12-05 16:00:49 +00:00
parent c6f3ead63e
commit 1f99bf931d
11 changed files with 1212 additions and 16 deletions

125
bin/app/Cargo.lock generated
View File

@@ -765,6 +765,15 @@ dependencies = [
"wyz",
]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "blake2b_simd"
version = "1.0.3"
@@ -1505,6 +1514,7 @@ dependencies = [
"async-trait",
"atomic_float",
"blake3",
"bs58",
"chrono",
"clap 4.5.53",
"colored",
@@ -1515,6 +1525,7 @@ dependencies = [
"file-rotate",
"fluent",
"freetype-rs",
"fud",
"futures",
"glam",
"harfbuzz-sys",
@@ -1909,6 +1920,33 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "dynasm"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7d4c414c94bc830797115b8e5f434d58e7e80cb42ba88508c14bc6ea270625"
dependencies = [
"bitflags 2.10.0",
"byteorder",
"lazy_static",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "dynasmrt"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "602f7458a3859195fb840e6e0cce5f4330dd9dfbfece0edaf31fe427af346f55"
dependencies = [
"byteorder",
"dynasm",
"fnv",
"memmap2",
]
[[package]]
name = "easy-parallel"
version = "3.3.1"
@@ -2053,6 +2091,19 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "equix"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89178c5241f5cc0c8f2b5ac5008f3c7a32caad341b1ec747a6e1e51d2e877110"
dependencies = [
"arrayvec",
"hashx",
"num-traits",
"thiserror 2.0.17",
"visibility",
]
[[package]]
name = "errno"
version = "0.3.14"
@@ -2228,6 +2279,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "fixed-capacity-vec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b31a14f5ee08ed1a40e1252b35af18bed062e3f39b69aab34decde36bc43e40"
[[package]]
name = "flate2"
version = "1.1.5"
@@ -2450,6 +2507,37 @@ dependencies = [
"winapi",
]
[[package]]
name = "fud"
version = "0.5.0"
dependencies = [
"async-trait",
"blake2",
"blake3",
"bs58",
"darkfi",
"darkfi-sdk",
"darkfi-serial",
"easy-parallel",
"equix",
"futures",
"num-bigint",
"rand 0.8.5",
"serde",
"sha2",
"signal-hook",
"signal-hook-async-std",
"sled-overlay",
"smol",
"structopt",
"structopt-toml",
"tinyjson",
"tracing",
"tracing-appender",
"tracing-subscriber",
"url",
]
[[package]]
name = "funty"
version = "2.0.0"
@@ -2815,6 +2903,21 @@ dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "hashx"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cb639748a589a17df2126f8015897ab416e81113afb82f56df5d47fa1486ab1"
dependencies = [
"arrayvec",
"blake2",
"dynasmrt",
"fixed-capacity-vec",
"hex",
"rand_core 0.9.3",
"thiserror 2.0.17",
]
[[package]]
name = "heck"
version = "0.3.3"
@@ -5181,6 +5284,28 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-async-std"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ae704a5db1c75caeec797eccb081a9980c8e374e8774c7cb7b26799cc727b38"
dependencies = [
"async-io",
"futures-lite",
"libc",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.7"

View File

@@ -27,6 +27,8 @@ zeromq = { version = "0.4.1", default-features = false, features = ["async-std-r
darkfi = {path = "../../", features = ["async-daemonize", "event-graph", "net", "util", "system", "zk"]}
#darkfi-sdk = {path = "../../src/sdk", features = ["async"]}
darkfi-serial = {version = "0.5.0", features = ["async"]}
fud = { path = "../fud/fud/" }
bs58 = "0.5.1"
thiserror = "2.0.12"
smol = "2.0.2"
atomic_float = "1.1.0"

View File

@@ -486,6 +486,8 @@ pub fn create_chatview(name: &str) -> SceneNode {
)
.unwrap();
node.add_method("update_file", vec![("hash", "Hash", CallArgType::Str)], None).unwrap();
node
}

View File

@@ -563,6 +563,7 @@ pub async fn make(
window_scale.clone(),
app.render_api.clone(),
app.text_shaper.clone(),
app.sg_root.clone(),
)
})
.await;

View File

@@ -283,6 +283,14 @@ async fn load_plugins(
})
.await;
let fud = create_fud("fud");
let sg_root2 = sg_root.clone();
let fud = fud
.setup(|me| async {
plugin::Fud::new(me, sg_root2, ex.clone()).await.expect("Fud pimpl setup")
})
.await;
let (slot, recvr) = Slot::new("recvmsg");
darkirc.register("recv", slot).unwrap();
let sg_root2 = sg_root.clone();
@@ -399,6 +407,7 @@ async fn load_plugins(
});
plugin.link(darkirc);
plugin.link(fud);
i!("Plugins loaded");
futures::join!(listen_recv, listen_connect);
@@ -446,6 +455,19 @@ pub fn create_darkirc(name: &str) -> SceneNode {
node
}
pub fn create_fud(name: &str) -> SceneNode {
t!("create_fud({name})");
let mut node = SceneNode::new(name, SceneNodeType::Plugin);
let mut prop = Property::new("ready", PropertyType::Bool, PropertySubType::Null);
prop.set_defaults_bool(vec![false]).unwrap();
node.add_property(prop).unwrap();
node.add_method("get", vec![("hash", "Hash", CallArgType::Str)], None).unwrap();
node
}
/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]

View File

@@ -32,6 +32,7 @@ pub const COLOR_LIGHTGREY: Color = [0.7, 0.7, 0.7, 1.];
pub const COLOR_GREEN: Color = [0., 1., 0., 1.];
pub const COLOR_BLUE: Color = [0., 0., 1., 1.];
pub const COLOR_PINK: Color = [0.8, 0.3, 0.8, 1.];
pub const COLOR_CYAN: Color = [0., 1., 1., 1.];
#[allow(dead_code)]
pub const COLOR_PURPLE: Color = [1., 0., 1., 1.];
pub const COLOR_WHITE: Color = [1., 1., 1., 1.];
@@ -112,6 +113,61 @@ impl MeshBuilder {
self.draw_box(obj, color, &uv);
}
pub fn draw_box_shadow(&mut self, obj: &Rectangle, color: Color, spread: f32) {
let (x1, y1) = obj.pos().unpack();
let (x2, y2) = obj.corner().unpack();
let uv = Rectangle::zero();
let (u1, v1) = uv.pos().unpack();
let (u2, v2) = uv.corner().unpack();
let color2 = [color[0], color[1], color[2], 0.];
// left
self.append(
vec![
Vertex { pos: [x1, y1], color, uv: [u1, v1] },
Vertex { pos: [x1 - spread, y1 - spread], color: color2, uv: [u2, v1] },
Vertex { pos: [x1, y2], color, uv: [u1, v2] },
Vertex { pos: [x1 - spread, y2 + spread], color: color2, uv: [u2, v2] },
],
vec![0, 2, 1, 1, 2, 3],
);
// top
self.append(
vec![
Vertex { pos: [x1, y1], color, uv: [u1, v1] },
Vertex { pos: [x1 - spread, y1 - spread], color: color2, uv: [u2, v1] },
Vertex { pos: [x2, y1], color, uv: [u1, v2] },
Vertex { pos: [x2 + spread, y1 - spread], color: color2, uv: [u2, v2] },
],
vec![0, 2, 1, 1, 2, 3],
);
// right
self.append(
vec![
Vertex { pos: [x2, y1], color, uv: [u1, v1] },
Vertex { pos: [x2 + spread, y1 - spread], color: color2, uv: [u2, v1] },
Vertex { pos: [x2, y2], color, uv: [u1, v2] },
Vertex { pos: [x2 + spread, y2 + spread], color: color2, uv: [u2, v2] },
],
vec![0, 2, 1, 1, 2, 3],
);
// bottom
self.append(
vec![
Vertex { pos: [x1, y2], color, uv: [u1, v1] },
Vertex { pos: [x1 - spread, y2 + spread], color: color2, uv: [u2, v1] },
Vertex { pos: [x2, y2], color, uv: [u1, v2] },
Vertex { pos: [x2 + spread, y2 + spread], color: color2, uv: [u2, v2] },
],
vec![0, 2, 1, 1, 2, 3],
);
}
pub fn draw_outline(&mut self, obj: &Rectangle, color: Color, thickness: f32) {
let (x1, y1) = obj.pos().unpack();
let (dist_x, dist_y) = (obj.w, obj.h);

507
bin/app/src/plugin/fud.rs Normal file
View File

@@ -0,0 +1,507 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2025 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use darkfi::{
net::{
session::{SESSION_DIRECT, SESSION_INBOUND},
settings::Settings as NetSettings,
P2p, P2pPtr,
},
system::{sleep, Publisher, PublisherPtr},
};
use darkfi_serial::{Decodable, Encodable};
use sled_overlay::sled;
use smol::lock::Mutex;
use std::{
collections::HashSet,
io::Cursor,
path::PathBuf,
sync::{Arc, OnceLock, Weak},
};
use url::Url;
use fud::{
event::FudEvent, proto::ProtocolFud, settings::Args as FudSettings, util::hash_to_string, Fud,
};
use crate::{
error::{Error, Result},
prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyBool, Role},
scene::{MethodCallSub, Pimpl, SceneNode, SceneNodePtr, SceneNodeType, SceneNodeWeak},
ui::OnModify,
ExecutorPtr,
};
use super::PluginSettings;
const P2P_RETRY_TIME: u64 = 20;
#[cfg(target_os = "android")]
mod paths {
use crate::android::{get_appdata_path, get_external_storage_path};
use std::path::PathBuf;
pub fn get_base_path() -> PathBuf {
get_external_storage_path().join("fud")
}
pub fn get_db_path() -> PathBuf {
get_external_storage_path().join("fud/db")
}
pub fn get_downloads_path() -> PathBuf {
get_external_storage_path().join("fud/downloads")
}
pub fn get_use_tor_filename() -> PathBuf {
get_external_storage_path().join("use_tor.txt")
}
pub fn p2p_datastore_path() -> PathBuf {
get_appdata_path().join("fud/p2p")
}
pub fn hostlist_path() -> PathBuf {
get_appdata_path().join("fud/hostlist.tsv")
}
}
#[cfg(not(target_os = "android"))]
mod paths {
use std::path::PathBuf;
pub fn get_base_path() -> PathBuf {
dirs::data_local_dir().unwrap().join("darkfi/app/fud")
}
pub fn get_db_path() -> PathBuf {
dirs::data_local_dir().unwrap().join("darkfi/app/fud/db")
}
pub fn get_downloads_path() -> PathBuf {
dirs::data_local_dir().unwrap().join("darkfi/app/fud/downloads")
}
pub fn get_use_tor_filename() -> PathBuf {
dirs::data_local_dir().unwrap().join("darkfi/app/use_tor.txt")
}
pub fn p2p_datastore_path() -> PathBuf {
dirs::cache_dir().unwrap().join("darkfi/app/fud/p2p")
}
pub fn hostlist_path() -> PathBuf {
dirs::cache_dir().unwrap().join("darkfi/app/fud/hostlist.tsv")
}
}
use paths::*;
macro_rules! t { ($($arg:tt)*) => { trace!(target: "plugin::fud", $($arg)*); } }
macro_rules! d { ($($arg:tt)*) => { debug!(target: "plugin::fud", $($arg)*); } }
macro_rules! i { ($($arg:tt)*) => { info!(target: "plugin::fud", $($arg)*); } }
macro_rules! e { ($($arg:tt)*) => { error!(target: "plugin::fud", $($arg)*); } }
pub type FudPluginPtr = Arc<FudPlugin>;
pub struct FudPlugin {
node: SceneNodeWeak,
sg_root: SceneNodePtr,
tasks: OnceLock<Vec<smol::Task<()>>>,
p2p: P2pPtr,
event_pub: PublisherPtr<FudEvent>,
fud: Arc<Fud>,
download_on_ready: Arc<Mutex<HashSet<Url>>>,
settings: PluginSettings,
}
impl FudPlugin {
pub async fn new(node: SceneNodeWeak, sg_root: SceneNodePtr, ex: ExecutorPtr) -> Result<Pimpl> {
let node_ref = &node.upgrade().unwrap();
// let fud_node_id = PropertyStr::wrap(node_ref, Role::Internal, "node_id", 0).unwrap();
let fud_ready = PropertyBool::wrap(node_ref, Role::Internal, "ready", 0).unwrap();
fud_ready.set(&mut PropertyAtomicGuard::none(), false);
let setting_root = Arc::new(SceneNode::new("setting", SceneNodeType::SettingRoot));
node_ref.clone().link(setting_root.clone());
let basedir = get_base_path();
i!("Starting Fud backend");
let db_path = get_db_path();
let db = match sled::open(&db_path) {
Ok(db) => db,
Err(err) => {
e!("Sled database '{}' failed to open: {err}!", db_path.display());
return Err(Error::SledDbErr)
}
};
let setting_tree = db.open_tree("settings")?;
let settings = PluginSettings { setting_root, sled_tree: setting_tree };
let mut fud_settings: FudSettings = Default::default();
fud_settings.base_dir = basedir.to_string_lossy().to_string();
let mut p2p_settings: NetSettings = fud_settings.net.clone().into();
p2p_settings.app_version = semver::Version::parse("0.5.0").unwrap();
if get_use_tor_filename().exists() {
i!("Setup P2P network [tor]");
p2p_settings.outbound_connect_timeout = 60;
p2p_settings.channel_handshake_timeout = 55;
p2p_settings.channel_heartbeat_interval = 90;
p2p_settings.outbound_peer_discovery_cooloff_time = 60;
p2p_settings.seeds.push(
url::Url::parse(
"tor://g7fxelebievvpr27w7gt24lflptpw3jeeuvafovgliq5utdst6xyruyd.onion:24442",
)
.unwrap(),
);
p2p_settings.seeds.push(
url::Url::parse(
"tor://yvklzjnfmwxhyodhrkpomawjcdvcaushsj6torjz2gyd7e25f3gfunyd.onion:24442",
)
.unwrap(),
);
p2p_settings.allowed_transports = vec!["tor".to_string()];
fud_settings.pow.btc_electrum_nodes.push(
url::Url::parse(
"tor://hezojf7rda2c33yxgcgcvvsxflechdz5vkm64gwlszgx2r4gc5e42kqd.onion:50001",
)
.unwrap(),
);
fud_settings.pow.btc_electrum_nodes.push(
url::Url::parse(
"tor://n4widoxtm3xpo2fjvtdffhb63q5td3utaxkolaegnpzb5khbwxvdrlad.onion:50001",
)
.unwrap(),
);
fud_settings.pow.btc_electrum_nodes.push(
url::Url::parse(
"tor://duras25aqnp3tnn2zgma7pusms6c7umtunyu2sp6e5byotr3c4c6rzad.onion:50001",
)
.unwrap(),
);
fud_settings.pow.btc_electrum_nodes.push(
url::Url::parse(
"tor://n3dz6thzxobyphuosoftgtf36rnsxlsjknke4yrbdys55zvd7nsx7qid.onion:50001",
)
.unwrap(),
);
} else {
i!("Setup P2P network [clearnet]");
p2p_settings.outbound_connect_timeout = 40;
p2p_settings.channel_handshake_timeout = 30;
p2p_settings.seeds.push(url::Url::parse("tcp+tls://lilith0.dark.fi:24441").unwrap());
p2p_settings.seeds.push(url::Url::parse("tcp+tls://lilith1.dark.fi:24441").unwrap());
fud_settings
.pow
.btc_electrum_nodes
.push(url::Url::parse("tcp+tls://erbium1.sytes.net:50002").unwrap());
fud_settings
.pow
.btc_electrum_nodes
.push(url::Url::parse("tcp+tls://ecdsa.net:110").unwrap());
fud_settings
.pow
.btc_electrum_nodes
.push(url::Url::parse("tcp+tls://electrum.no-ip.org:50002").unwrap());
fud_settings
.pow
.btc_electrum_nodes
.push(url::Url::parse("tcp+tls://electrumx.not.fyi:50002").unwrap());
}
p2p_settings.p2p_datastore = p2p_datastore_path().into_os_string().into_string().ok();
p2p_settings.hostlist = hostlist_path().into_os_string().into_string().ok();
settings.add_p2p_settings(&p2p_settings);
// TODO: add other fud settings
settings.load_settings();
settings.update_p2p_settings(&mut p2p_settings);
let p2p = match P2p::new(p2p_settings.clone(), ex.clone()).await {
Ok(p2p) => p2p,
Err(err) => {
e!("Create p2p network failed: {err}!");
return Err(Error::ServiceFailed)
}
};
p2p.session_direct().start_peer_discovery();
let event_pub = Publisher::new();
let fud: Arc<Fud> =
match Fud::new(fud_settings, p2p.clone(), &db, event_pub.clone(), ex.clone()).await {
Ok(fud) => fud,
Err(err) => {
e!("Cannot create fud instance: {err}");
return Err(Error::ServiceFailed)
}
};
let self_ = Arc::new(Self {
node: node.clone(),
sg_root,
tasks: OnceLock::new(),
p2p,
event_pub,
fud,
download_on_ready: Arc::new(Mutex::new(HashSet::new())),
settings,
});
self_.clone().start(ex).await;
Ok(Pimpl::Fud(self_))
}
async fn apply_settings(self_: Arc<Self>, _batch: BatchGuardPtr) {
self_.settings.save_settings();
let p2p_settings = self_.p2p.settings();
let mut write_guard = p2p_settings.write().await;
self_.settings.update_p2p_settings(&mut write_guard);
// TODO: add other fud settings
}
async fn start(self: Arc<Self>, ex: ExecutorPtr) {
i!("Registering Fud protocol");
let registry = self.p2p.protocol_registry();
let fud = self.fud.clone();
let p2p = self.p2p.clone();
registry
.register(SESSION_DIRECT | SESSION_INBOUND, move |channel, _| {
let fud_ = fud.clone();
let p2p_ = p2p.clone();
async move { ProtocolFud::init(fud_, channel, p2p_).await.unwrap() }
})
.await;
let me = Arc::downgrade(&self);
let node = &self.node.upgrade().unwrap();
let method_sub = node.subscribe_method_call("get").unwrap();
let me2 = me.clone();
let get_method_task =
ex.spawn(async move { while Self::process_get(&me2, &method_sub).await {} });
let event_pub = self.event_pub.clone();
let me2 = me.clone();
let ev_task = ex.spawn(async move {
Self::process_events(&me2, event_pub).await;
});
let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
// `apply_settings` is triggered if any setting changes
for setting_node in self.settings.setting_root.get_children().iter() {
on_modify.when_change(
setting_node.get_property("value").clone().unwrap(),
Self::apply_settings,
);
}
let fud = self.fud.clone();
let start_task = ex.spawn(async move {
while fud.start().await.is_err() {
sleep(10).await;
}
});
let mut tasks = vec![get_method_task, ev_task, start_task];
tasks.append(&mut on_modify.tasks);
self.tasks.set(tasks).unwrap();
i!("Starting Fud P2P");
while let Err(err) = self.p2p.clone().start().await {
// This usually means we cannot listen on the inbound ports
e!("Failed to start fud's p2p network: {err}!");
e!("Usually this means there is another process listening on the same ports.");
e!("Trying again in {P2P_RETRY_TIME} secs");
sleep(P2P_RETRY_TIME).await;
}
}
fn string_to_hash(str: &str) -> std::io::Result<blake3::Hash> {
let mut hash_buf = vec![];
match bs58::decode(str).onto(&mut hash_buf) {
Ok(_) => {}
Err(_) => {
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Invalid fud hash"))
}
}
if hash_buf.len() != 32 {
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Invalid fud hash"))
}
let mut hash_buf_arr = [0u8; 32];
hash_buf_arr.copy_from_slice(&hash_buf);
Ok(blake3::Hash::from_bytes(hash_buf_arr))
}
async fn process_get(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
let Ok(method_call) = sub.receive().await else {
d!("Fud event relayer closed");
return false
};
t!("method called: get({method_call:?})");
assert!(method_call.send_res.is_none());
fn decode_data(data: &[u8]) -> std::io::Result<(String, Url)> {
let mut cur = Cursor::new(&data);
let url = Url::decode(&mut cur)?;
let Some(hash_string) = url.host_str().clone() else {
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Missing fud hash"))
};
let hash_string = hash_string.to_string();
Ok((hash_string, url))
}
let Some(self_) = me.upgrade() else {
// Should not happen
panic!("self destroyed before get_method_task was stopped!");
};
let Ok((hash_string, url)) = decode_data(&method_call.data) else {
e!("get() method invalid arg data");
return true
};
let Ok(hash) = FudPlugin::string_to_hash(&hash_string) else {
let mut data = vec![];
"invalid fud url".encode(&mut data).unwrap();
self_.update_file(hash_string, "error", data).await;
return true
};
if self_.node.upgrade().unwrap().get_property_bool("ready").unwrap() {
let file_selection = match url.path() {
"/" | "" => fud::util::FileSelection::All,
path => {
let mut selection = HashSet::new();
selection.insert(PathBuf::from(path.strip_prefix("/").unwrap_or(path)));
fud::util::FileSelection::Set(selection)
}
};
let _ = self_
.fud
.get(&hash, &get_downloads_path().join(&hash_string), file_selection)
.await;
} else {
self_.download_on_ready.lock().await.insert(url);
}
true
}
async fn update_file(self: &Arc<Self>, hash: String, status: &str, encoded_data: Vec<u8>) {
let window = self.sg_root.lookup_node("/window");
if window.is_none() {
return
}
let window = window.unwrap();
for child in window.get_children() {
if let Some(chatty) = child.lookup_node("/content/chatty") {
let mut data = vec![];
hash.encode(&mut data).unwrap();
status.encode(&mut data).unwrap();
data.extend(encoded_data.clone());
let _ = chatty.call_method("update_file", data).await;
}
}
}
async fn process_events(me: &Weak<Self>, publisher: PublisherPtr<FudEvent>) {
let Some(self_) = me.upgrade() else {
// Should not happen
panic!("self destroyed before ev_task was stopped!");
};
let sub = publisher.subscribe().await;
loop {
match sub.receive().await {
FudEvent::Ready => {
let atom = &mut PropertyAtomicGuard::none();
self_
.node
.upgrade()
.unwrap()
.set_property_bool(atom, Role::App, "ready", true)
.unwrap();
let window = self_.sg_root.lookup_node("/window");
if window.is_none() {
continue
}
for url in self_.download_on_ready.lock().await.iter() {
let mut data = vec![];
url.encode(&mut data).unwrap();
let _ = self_.node.upgrade().unwrap().call_method("get", data).await;
}
}
FudEvent::DownloadStarted(ev) => {
let mut data = vec![];
let bytes_downloaded = ev.resource.target_bytes_downloaded as f32;
let bytes_size = ev.resource.target_bytes_size as f32;
let progress =
if bytes_size != 0.0 { bytes_downloaded / bytes_size * 100.0 } else { 0.0 };
progress.encode(&mut data).unwrap();
self_.update_file(hash_to_string(&ev.resource.hash), "downloading", data).await;
}
FudEvent::ChunkDownloadCompleted(ev) => {
let mut data = vec![];
let bytes_downloaded = ev.resource.target_bytes_downloaded as f32;
let bytes_size = ev.resource.target_bytes_size as f32;
let progress =
if bytes_size != 0.0 { bytes_downloaded / bytes_size * 100.0 } else { 0.0 };
progress.encode(&mut data).unwrap();
self_.update_file(hash_to_string(&ev.resource.hash), "downloading", data).await;
}
FudEvent::DownloadCompleted(ev) => {
let mut data = vec![];
let path_string = ev.resource.path.to_string_lossy().to_string();
path_string.encode(&mut data).unwrap();
self_.update_file(hash_to_string(&ev.resource.hash), "downloaded", data).await;
}
FudEvent::DownloadError(ev) => {
let mut data = vec![];
ev.error.encode(&mut data).unwrap();
self_.update_file(hash_to_string(&ev.hash), "error", data).await;
}
FudEvent::MissingChunks(ev) => {
let mut data = vec![];
"missing chunks".encode(&mut data).unwrap();
self_.update_file(hash_to_string(&ev.hash), "error", data).await;
}
FudEvent::MetadataNotFound(ev) => {
let mut data = vec![];
"missing metadata".encode(&mut data).unwrap();
self_.update_file(hash_to_string(&ev.hash), "error", data).await;
}
_ => {}
};
}
}
}

View File

@@ -24,6 +24,9 @@ pub mod darkirc;
pub use darkirc::DarkIrc;
pub use darkirc::DarkIrcPtr;
pub mod fud;
pub use fud::{FudPlugin as Fud, FudPluginPtr as FudPtr};
use darkfi::net::Settings as NetSettings;
use crate::{

View File

@@ -546,6 +546,7 @@ pub enum Pimpl {
Gesture(ui::GesturePtr),
EmojiPicker(ui::EmojiPickerPtr),
DarkIrc(plugin::DarkIrcPtr),
Fud(plugin::FudPtr),
}
impl std::fmt::Debug for Pimpl {

View File

@@ -24,6 +24,7 @@ use darkfi_serial::{deserialize, Decodable, Encodable, SerialDecodable, SerialEn
use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
use parking_lot::Mutex as SyncMutex;
use rand::{rngs::OsRng, Rng};
use regex::Regex;
use sled_overlay::sled;
use std::{
collections::VecDeque,
@@ -34,9 +35,10 @@ use std::{
},
};
use tracing::instrument;
use url::Url;
mod page;
use page::MessageBuffer;
use page::{FileMessageStatus, MessageBuffer};
use crate::{
gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi},
@@ -44,7 +46,7 @@ use crate::{
BatchGuardId, BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor,
PropertyFloat32, PropertyRect, PropertyUint32, Role,
},
scene::{MethodCallSub, Pimpl, SceneNodeWeak},
scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
text::TextShaperPtr,
ExecutorPtr,
};
@@ -73,6 +75,11 @@ fn max(a: f32, b: f32) -> f32 {
}
}
fn get_file_url(text: &String) -> Option<Url> {
let url_regex = Regex::new(r"fud://[^\s]+").unwrap();
url_regex.find(text).and_then(|match_| Url::parse(match_.as_str()).ok())
}
#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
pub struct ChatMsg {
pub nick: String,
@@ -147,6 +154,7 @@ pub struct ChatView {
node: SceneNodeWeak,
tasks: SyncMutex<Vec<smol::Task<()>>>,
render_api: RenderApi,
sg_root: SceneNodePtr,
tree: sled::Tree,
msgbuf: AsyncMutex<MessageBuffer>,
@@ -189,6 +197,7 @@ impl ChatView {
window_scale: PropertyFloat32,
render_api: RenderApi,
text_shaper: TextShaperPtr,
sg_root: SceneNodePtr,
) -> Pimpl {
t!("ChatView::new()");
@@ -230,6 +239,7 @@ impl ChatView {
node: node.clone(),
tasks: SyncMutex::new(vec![]),
render_api: render_api.clone(),
sg_root,
tree,
msgbuf: AsyncMutex::new(MessageBuffer::new(
@@ -444,11 +454,31 @@ impl ChatView {
t!("Mark sent message as confirmed");
} else {
t!("Inserting new message");
// Insert the privmsg since it doesn't already exist
if msgbuf.insert_privmsg(timest, msg_id, nick, text).is_none() {
let privmsg = msgbuf.insert_privmsg(timest, msg_id.clone(), nick.clone(), text.clone());
if privmsg.is_none() {
// Not visible so no need to redraw
return
}
if let Some(url) = get_file_url(&text) {
if let Some(fud) = self.sg_root.lookup_node("/plugin/fud") {
msgbuf.insert_filemsg(
timest,
msg_id,
FileMessageStatus::Initializing,
nick,
url.clone(),
);
let mut data = vec![];
url.encode(&mut data).unwrap();
fud.call_method("get", data).await.unwrap();
}
} else {
error!(target: "ui::chatview", "Fud plugin has not been loaded");
}
}
let atom = self.render_api.make_guard(gfxtag!("ChatView::handle_insert_line"));
@@ -559,6 +589,11 @@ impl ChatView {
}
};
let Some(fud) = self.sg_root.lookup_node("/plugin/fud") else {
error!(target: "ui::chatview", "Fud plugin has not been loaded");
return
};
let mut do_redraw = false;
for entry in iter {
let Ok((k, v)) = entry else { break };
@@ -569,7 +604,26 @@ impl ChatView {
let chatmsg: ChatMsg = deserialize(&v).unwrap();
//t!("{timest:?} {chatmsg:?} [trace_id={trace_id}]");
let msg_height = msgbuf.push_privmsg(timest, msg_id, chatmsg.nick, chatmsg.text);
let msg_height = msgbuf.push_privmsg(
timest,
msg_id.clone(),
chatmsg.nick.clone(),
chatmsg.text.clone(),
);
if let Some(url) = get_file_url(&chatmsg.text) {
msgbuf.insert_filemsg(
timest,
msg_id,
FileMessageStatus::Initializing,
chatmsg.nick.clone(),
url.clone(),
);
let mut data = vec![];
url.encode(&mut data).unwrap();
fud.call_method("get", data).await.unwrap();
}
remaining_load_height -= msg_height;
if remaining_load_height <= 0. {
@@ -754,6 +808,22 @@ impl UIObject for ChatView {
}
});
let method_sub = node_ref.subscribe_method_call("update_file").unwrap();
let self_ = self.clone();
let update_file_task = ex.spawn(async move {
loop {
let Ok(method_call) = method_sub.receive().await else {
d!("Event relayer closed");
return
};
let mut msgbuf = self_.msgbuf.lock().await;
msgbuf.update_file(&method_call.data).await;
msgbuf.adjust_params();
let atom = self_.render_api.make_guard(gfxtag!("ChatView::update_file_task"));
self_.redraw_cached(atom.batch_id, &mut msgbuf).await;
}
});
let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
async fn reload_view(self_: Arc<ChatView>, batch: BatchGuardPtr) {
@@ -783,8 +853,13 @@ impl UIObject for ChatView {
on_modify.when_change(self.rect.prop(), redraw);
//on_modify.when_change(self.debug.prop(), redraw);
let mut tasks =
vec![insert_line_method_task, insert_unconf_line_method_task, motion_task, bgload_task];
let mut tasks = vec![
insert_line_method_task,
insert_unconf_line_method_task,
motion_task,
bgload_task,
update_file_task,
];
tasks.append(&mut on_modify.tasks);
*self.tasks.lock() = tasks;

View File

@@ -17,18 +17,27 @@
*/
use async_gen::{gen as async_gen, AsyncIter};
use async_trait::async_trait;
use chrono::{Local, NaiveDate, TimeZone};
use darkfi_serial::{Decodable, FutAsyncWriteExt, SerialDecodable, SerialEncodable};
use futures::stream::{Stream, StreamExt};
use image::{ImageBuffer, ImageReader, Rgba};
use parking_lot::Mutex as SyncMutex;
use std::{
collections::HashMap,
hash::{DefaultHasher, Hash, Hasher},
io::Cursor,
pin::pin,
sync::Arc,
};
use url::Url;
use super::{max, MessageId, Timestamp};
use crate::{
gfx::{gfxtag, DrawMesh, Rectangle, RenderApi},
mesh::{Color, MeshBuilder, COLOR_BLUE, COLOR_PINK, COLOR_WHITE},
gfx::{gfxtag, DrawMesh, ManagedTexturePtr, Rectangle, RenderApi},
mesh::{
Color, MeshBuilder, COLOR_BLUE, COLOR_CYAN, COLOR_GREEN, COLOR_PINK, COLOR_RED, COLOR_WHITE,
},
prop::{PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr},
text::{self, Glyph, GlyphPositionIter, TextShaper, TextShaperPtr},
util::enumerate_mut,
@@ -484,11 +493,300 @@ impl std::fmt::Debug for DateMessage {
}
}
#[derive(Clone, SerialEncodable, SerialDecodable)]
pub enum FileMessageStatus {
Initializing,
Downloading { progress: f32 },
Downloaded { path: String },
Error { msg: String },
}
type GenericImageBuffer = ImageBuffer<Rgba<u8>, Vec<u8>>;
#[derive(Clone)]
pub struct FileMessage {
font_size: f32,
window_scale: f32,
max_width: f32,
file_url: Url,
status: FileMessageStatus,
imgbuf: Arc<SyncMutex<Option<GenericImageBuffer>>>,
timestamp: Timestamp,
glyphs: Vec<Vec<Glyph>>,
atlas: text::RenderedAtlas,
}
impl FileMessage {
const GLOW_SIZE: f32 = 20.;
const MARGIN_TOP: f32 = 4.;
const MARGIN_BOTTOM: f32 = 10.;
const BOX_PADDING_TOP: f32 = 15.;
const BOX_PADDING_BOTTOM: f32 = 8.;
const BOX_PADDING_X: f32 = 15.;
const IMG_MAX_HEIGHT: f32 = 500.;
pub fn new(
font_size: f32,
window_scale: f32,
file_url: Url,
status: FileMessageStatus,
timestamp: Timestamp,
_nick: String,
text_shaper: &TextShaper,
render_api: &RenderApi,
) -> Message {
let mut glyphs = Vec::new();
let mut atlas = text::Atlas::new(render_api, gfxtag!("chatview_filemsg"));
for str in Self::filestr(&file_url, &status) {
let glyphs_ = text_shaper.shape(str, font_size, window_scale);
atlas.push(&glyphs_);
glyphs.push(glyphs_);
}
let atlas = atlas.make();
Message::File(Self {
font_size,
window_scale,
max_width: 0.,
file_url,
status,
imgbuf: Arc::new(SyncMutex::new(None)),
timestamp,
glyphs,
atlas,
})
}
fn filestr(file_url: &Url, status: &FileMessageStatus) -> Vec<String> {
let status_str = match status {
FileMessageStatus::Initializing => "starting fud".to_string(),
FileMessageStatus::Downloading { progress } => format!("downloading [{progress:.1}%]"),
FileMessageStatus::Downloaded { .. } => "downloaded".to_string(),
FileMessageStatus::Error { msg } => msg.to_lowercase(),
};
vec![
file_url
.host_str()
.map(|file_hash| {
if file_hash.len() >= 12 {
let first_part = &file_hash[..4];
let last_part = &file_hash[file_hash.len() - 4..];
format!("{}...{}", first_part, last_part)
} else {
file_hash.to_string()
}
})
.unwrap_or("???".to_string()),
status_str,
]
}
pub fn set_status(&mut self, status: &FileMessageStatus) {
self.status = status.clone();
if let FileMessageStatus::Downloaded { .. } = status {
let mut imgbuf = self.imgbuf.lock();
*imgbuf = self.load_img();
}
}
fn adjust_params(
&mut self,
font_size: f32,
window_scale: f32,
text_shaper: &TextShaper,
render_api: &RenderApi,
) {
self.font_size = font_size;
self.window_scale = window_scale;
self.glyphs = Vec::new();
let mut atlas = text::Atlas::new(render_api, gfxtag!("chatview_filemsg"));
for str in Self::filestr(&self.file_url, &self.status) {
let glyphs = text_shaper.shape(str, font_size, window_scale);
atlas.push(&glyphs);
self.glyphs.push(glyphs);
}
self.atlas = atlas.make();
}
fn adjust_width(&mut self, line_width: f32, timestamp_width: f32) {
let width = line_width - timestamp_width;
// clamp to > 0
self.max_width = max(width, 0.);
}
fn clear_mesh(&mut self) {}
fn get_img_size(&self, imgbuf: &ImageBuffer<Rgba<u8>, Vec<u8>>) -> (f32, f32) {
let img_w = imgbuf.width() as f32;
let img_h = imgbuf.height() as f32;
let width_scale = (self.max_width - Self::GLOW_SIZE) / img_w;
let height_scale = Self::IMG_MAX_HEIGHT / img_h;
let scale = width_scale.min(height_scale);
(img_w * scale, img_h * scale)
}
fn gen_mesh(
&mut self,
_clip: &Rectangle,
line_height: f32,
baseline: f32,
timestamp_width: f32,
_nick_colors: &[Color],
timestamp_color: Color,
_text_color: Color,
_debug_render: bool,
render_api: &RenderApi,
) -> Vec<DrawMesh> {
let uv_rect = Rectangle::from([0., 0., 1., 1.]);
let imgbuf_ = self.imgbuf.lock();
if let Some(ref imgbuf) = *imgbuf_ {
let (img_w, img_h) = self.get_img_size(imgbuf);
drop(imgbuf_);
let mesh_rect =
Rectangle::from([timestamp_width, -img_h - Self::MARGIN_BOTTOM, img_w, img_h]);
let texture = self.load_texture(render_api);
let mut mesh_gradient = MeshBuilder::new(gfxtag!("file_gradient"));
let glow_color = [timestamp_color[0], timestamp_color[1], timestamp_color[2], 0.5];
mesh_gradient.draw_box_shadow(&mesh_rect, glow_color, Self::GLOW_SIZE);
let mesh_gradient = mesh_gradient.alloc(render_api);
let mesh_gradient = mesh_gradient.draw_untextured();
let mut mesh_img = MeshBuilder::new(gfxtag!("file_img"));
mesh_img.draw_box(&mesh_rect, COLOR_WHITE, &uv_rect);
let mesh_img = mesh_img.alloc(render_api);
let mesh_img = mesh_img.draw_with_texture(texture);
return vec![mesh_img, mesh_gradient];
}
drop(imgbuf_);
let mut mesh = MeshBuilder::new(gfxtag!("chatview_filemsg"));
let color = match self.status {
FileMessageStatus::Initializing => timestamp_color,
FileMessageStatus::Downloading { .. } => COLOR_CYAN,
FileMessageStatus::Downloaded { .. } => COLOR_GREEN,
FileMessageStatus::Error { .. } => COLOR_RED,
};
let mut text_width = 0.;
for (i, glyphs) in self.glyphs.iter().enumerate() {
let glyph_pos_iter =
GlyphPositionIter::new(self.font_size, self.window_scale, &glyphs, baseline);
for (mut glyph_rect, glyph) in glyph_pos_iter.zip(glyphs.iter()) {
let uv_rect = self.atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
if glyph_rect.x + glyph_rect.w > text_width {
text_width = glyph_rect.x + glyph_rect.w;
}
glyph_rect.x += timestamp_width + Self::BOX_PADDING_X;
glyph_rect.y -= line_height * (self.glyphs.len() - i) as f32 +
Self::BOX_PADDING_BOTTOM +
Self::MARGIN_BOTTOM;
mesh.draw_box(&glyph_rect, color, uv_rect);
}
}
let box_width = text_width + Self::BOX_PADDING_X * 2.;
let box_height = self.glyphs.len() as f32 * line_height +
Self::BOX_PADDING_TOP +
Self::BOX_PADDING_BOTTOM;
let mesh_rect = Rectangle::from([
timestamp_width,
-box_height - Self::MARGIN_BOTTOM,
box_width,
box_height,
]);
mesh.draw_outline(&mesh_rect, color, 1.);
let glow_color = [color[0], color[1], color[2], 0.3];
mesh.draw_box_shadow(&mesh_rect, glow_color, Self::GLOW_SIZE);
let mesh = mesh.alloc(render_api);
let mesh = mesh.draw_with_texture(self.atlas.texture.clone());
vec![mesh]
}
fn load_img(&self) -> Option<ImageBuffer<Rgba<u8>, Vec<u8>>> {
if let FileMessageStatus::Downloaded { path } = &self.status {
let path = path.as_str();
let data = Arc::new(SyncMutex::new(vec![]));
let data2 = data.clone();
miniquad::fs::load_file(path, move |res| match res {
Ok(res) => *data2.lock() = res,
Err(e) => {
error!("Resource not found! {e}");
}
});
let data = std::mem::take(&mut *data.lock());
let Ok(img) =
ImageReader::new(Cursor::new(data)).with_guessed_format().unwrap().decode()
else {
return None;
};
return Some(img.to_rgba8());
}
None
}
fn load_texture(&self, render_api: &RenderApi) -> ManagedTexturePtr {
let imgbuf = self.imgbuf.lock();
let img = imgbuf.as_ref().unwrap();
let width = img.width() as u16;
let height = img.height() as u16;
let bmp = img.as_raw().clone();
drop(imgbuf);
render_api.new_texture(width, height, bmp, gfxtag!("file_img_texture"))
}
pub fn height(&self, line_height: f32) -> f32 {
let imgbuf = self.imgbuf.lock();
imgbuf
.as_ref()
.map(|buf| self.get_img_size(buf).1 as f32 + Self::MARGIN_TOP + Self::MARGIN_BOTTOM)
.unwrap_or(
line_height * self.glyphs.len() as f32 +
Self::BOX_PADDING_TOP +
Self::BOX_PADDING_BOTTOM +
Self::MARGIN_TOP +
Self::MARGIN_BOTTOM,
)
}
fn select(&mut self) {}
}
impl std::fmt::Debug for FileMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "file: {}", self.file_url)
}
}
/// Easier than fucking around with traits nonsense
#[derive(Debug)]
pub enum Message {
Priv(PrivMessage),
Date(DateMessage),
File(FileMessage),
}
impl Message {
@@ -496,6 +794,7 @@ impl Message {
match self {
Self::Priv(m) => m.timestamp,
Self::Date(m) => m.timestamp,
Self::File(m) => m.timestamp,
}
}
@@ -503,6 +802,7 @@ impl Message {
match self {
Self::Priv(m) => m.height(line_height),
Self::Date(_) => line_height,
Self::File(m) => m.height(line_height),
}
}
@@ -527,6 +827,7 @@ impl Message {
render_api,
),
Self::Date(m) => m.adjust_params(font_size, window_scale, text_shaper, render_api),
Self::File(m) => m.adjust_params(font_size, window_scale, text_shaper, render_api),
}
}
@@ -534,6 +835,7 @@ impl Message {
match self {
Self::Priv(m) => m.adjust_width(line_width, timestamp_width),
Self::Date(_) => {}
Self::File(m) => m.adjust_width(line_width, timestamp_width),
}
}
@@ -541,6 +843,7 @@ impl Message {
match self {
Self::Priv(m) => m.clear_mesh(),
Self::Date(m) => m.clear_mesh(),
Self::File(m) => m.clear_mesh(),
}
}
@@ -557,9 +860,9 @@ impl Message {
hi_bg_color: Color,
debug_render: bool,
render_api: &RenderApi,
) -> DrawMesh {
) -> Vec<DrawMesh> {
match self {
Self::Priv(m) => m.gen_mesh(
Self::Priv(m) => vec![m.gen_mesh(
clip,
line_height,
msg_spacing,
@@ -571,8 +874,8 @@ impl Message {
hi_bg_color,
debug_render,
render_api,
),
Self::Date(m) => m.gen_mesh(
)],
Self::Date(m) => vec![m.gen_mesh(
clip,
line_height,
baseline,
@@ -583,6 +886,17 @@ impl Message {
// No hi_bg_color since dates can't be highlighted
debug_render,
render_api,
)],
Self::File(m) => m.gen_mesh(
clip,
line_height,
baseline,
timestamp_width,
nick_colors,
timestamp_color,
text_color,
debug_render,
render_api,
),
}
}
@@ -591,6 +905,7 @@ impl Message {
match self {
Self::Priv(_) => false,
Self::Date(_) => true,
Self::File(_) => false,
}
}
@@ -598,6 +913,7 @@ impl Message {
match self {
Self::Priv(m) => m.select(),
Self::Date(_) => {}
Self::File(m) => m.select(),
}
}
@@ -607,6 +923,13 @@ impl Message {
_ => None,
}
}
fn get_filemsg_mut(&mut self) -> Option<&mut FileMessage> {
match self {
Message::File(msg) => Some(msg),
_ => None,
}
}
}
fn select_nick_color(nick: &str, nick_colors: &[Color]) -> Color {
@@ -925,7 +1248,7 @@ impl MessageBuffer {
continue
}
let mesh = msg.gen_mesh(
for mesh in msg.gen_mesh(
rect,
line_height,
msg_spacing,
@@ -937,9 +1260,9 @@ impl MessageBuffer {
hi_bg_color,
debug_render,
&render_api,
);
meshes.push((current_pos, mesh));
) {
meshes.push((current_pos, mesh));
}
current_pos += msg_spacing;
current_pos += mesh_height;
@@ -949,6 +1272,50 @@ impl MessageBuffer {
meshes
}
pub fn insert_filemsg(
&mut self,
timest: Timestamp,
msg_id: MessageId,
status: FileMessageStatus,
nick: String,
file_url: Url,
) -> Option<&mut FileMessage> {
t!("insert_filemsg({timest}, {msg_id}, {nick}, {file_url})");
let font_size = self.font_size.get();
let window_scale = self.window_scale.get();
let msg = FileMessage::new(
font_size,
window_scale,
file_url,
status,
timest,
nick,
&self.text_shaper,
&self.render_api,
);
// Timestamps go from most recent backwards
let mut idx = None;
for (i, msg) in enumerate_mut(&mut self.msgs) {
if timest >= msg.timestamp() {
idx = Some(i);
break
}
}
let idx = match idx {
Some(idx) => idx,
None => {
let last_page_idx = 0;
last_page_idx
}
};
self.msgs.insert(idx, msg);
self.msgs[idx].get_filemsg_mut()
}
/// Gets around borrow checker with unsafe
fn msgs_with_date(&mut self) -> impl Stream<Item = &mut Message> {
let font_size = self.font_size.get();
@@ -1042,6 +1409,7 @@ impl MessageBuffer {
}
msg.select();
msg.clear_mesh();
break
}
@@ -1050,4 +1418,38 @@ impl MessageBuffer {
current_pos += mesh_height;
}
}
pub async fn update_file(&mut self, data: &Vec<u8>) {
let mut cur = Cursor::new(data);
let hash = String::decode(&mut cur).unwrap();
let status = String::decode(&mut cur).unwrap();
let status = match status.as_str() {
"downloading" => {
let progress = f32::decode(&mut cur).unwrap();
FileMessageStatus::Downloading { progress }
}
"downloaded" => {
let path = String::decode(&mut cur).unwrap();
FileMessageStatus::Downloaded { path }
}
"error" => {
let msg = String::decode(&mut cur).unwrap();
FileMessageStatus::Error { msg }
}
_ => FileMessageStatus::Initializing,
};
// TODO: keep a cache of file messages somewhere to avoid looping
// over all messages
for msg in &mut self.msgs {
if let Some(filemsg) = msg.get_filemsg_mut() {
if filemsg.file_url.host_str() == Some(&hash) {
filemsg.set_status(&status);
filemsg.adjust_width(self.line_width, self.timestamp_width.get());
filemsg.clear_mesh();
}
}
}
}
}