Merge 'Support view, key storage'
kernelkind (5):
file storage
write log files to disk daily and on panic
app window size persists on app close
support view
fix cmd line args bug
This commit is contained in:
131
Cargo.lock
generated
131
Cargo.lock
generated
@@ -18,6 +18,16 @@ version = "0.1.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
|
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "accesskit"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74a4b14f3d99c1255dcba8f45621ab1a2e7540a0009652d33989005a4d0bfc6b"
|
||||||
|
dependencies = [
|
||||||
|
"enumn",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.24.1"
|
version = "0.24.1"
|
||||||
@@ -69,6 +79,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"serde",
|
||||||
"version_check",
|
"version_check",
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
@@ -917,6 +928,15 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@@ -991,6 +1011,27 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1028,6 +1069,7 @@ source = "git+https://github.com/emilk/egui?rev=fcb7764e48ce00f8f8e58da10f937410
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"emath",
|
"emath",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1068,12 +1110,14 @@ name = "egui"
|
|||||||
version = "0.27.2"
|
version = "0.27.2"
|
||||||
source = "git+https://github.com/emilk/egui?rev=fcb7764e48ce00f8f8e58da10f937410d65b0bfb#fcb7764e48ce00f8f8e58da10f937410d65b0bfb"
|
source = "git+https://github.com/emilk/egui?rev=fcb7764e48ce00f8f8e58da10f937410d65b0bfb#fcb7764e48ce00f8f8e58da10f937410d65b0bfb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"accesskit",
|
||||||
"ahash",
|
"ahash",
|
||||||
"emath",
|
"emath",
|
||||||
"epaint",
|
"epaint",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
"puffin",
|
"puffin",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1211,6 +1255,7 @@ version = "0.27.2"
|
|||||||
source = "git+https://github.com/emilk/egui?rev=fcb7764e48ce00f8f8e58da10f937410d65b0bfb#fcb7764e48ce00f8f8e58da10f937410d65b0bfb"
|
source = "git+https://github.com/emilk/egui?rev=fcb7764e48ce00f8f8e58da10f937410d65b0bfb#fcb7764e48ce00f8f8e58da10f937410d65b0bfb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1249,6 +1294,17 @@ dependencies = [
|
|||||||
"syn 2.0.77",
|
"syn 2.0.77",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumn"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.77",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -1299,6 +1355,7 @@ dependencies = [
|
|||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"puffin",
|
"puffin",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1358,6 +1415,12 @@ dependencies = [
|
|||||||
"zune-inflate",
|
"zune-inflate",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fdeflate"
|
name = "fdeflate"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -2183,6 +2246,16 @@ dependencies = [
|
|||||||
"redox_syscall 0.4.1",
|
"redox_syscall 0.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -2503,6 +2576,7 @@ dependencies = [
|
|||||||
"base32",
|
"base32",
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
|
"dirs",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
@@ -2527,10 +2601,13 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tracing-wasm",
|
"tracing-wasm",
|
||||||
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"winit",
|
"winit",
|
||||||
@@ -2816,13 +2893,19 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orbclient"
|
name = "orbclient"
|
||||||
version = "0.3.47"
|
version = "0.3.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166"
|
checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libredox",
|
"libredox 0.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3333,6 +3416,17 @@ dependencies = [
|
|||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libredox 0.1.3",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.6"
|
version = "1.10.6"
|
||||||
@@ -3500,9 +3594,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.36"
|
version = "0.38.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36"
|
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"errno",
|
"errno",
|
||||||
@@ -4076,6 +4170,19 @@ version = "0.12.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"fastrand",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@@ -4358,6 +4465,18 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-appender"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.27"
|
version = "0.1.27"
|
||||||
@@ -4561,6 +4680,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usvg"
|
name = "usvg"
|
||||||
version = "0.37.0"
|
version = "0.37.0"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ crate-type = ["lib", "cdylib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
#egui-android = { git = "https://github.com/jb55/egui-android.git" }
|
#egui-android = { git = "https://github.com/jb55/egui-android.git" }
|
||||||
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb" }
|
egui = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", features = ["serde"] }
|
||||||
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe", default-features = false, features = [ "wgpu", "wayland", "x11", "android-native-activity" ] }
|
eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "eframe", default-features = false, features = [ "wgpu", "wayland", "x11", "android-native-activity" ] }
|
||||||
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
|
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
|
||||||
ehttp = "0.2.0"
|
ehttp = "0.2.0"
|
||||||
@@ -43,7 +43,12 @@ strum_macros = "0.26"
|
|||||||
bitflags = "2.5.0"
|
bitflags = "2.5.0"
|
||||||
uuid = { version = "1.10.0", features = ["v4"] }
|
uuid = { version = "1.10.0", features = ["v4"] }
|
||||||
indexmap = "2.6.0"
|
indexmap = "2.6.0"
|
||||||
|
dirs = "5.0.1"
|
||||||
|
tracing-appender = "0.2.3"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.13.0"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
security-framework = "2.11.0"
|
security-framework = "2.11.0"
|
||||||
|
|||||||
BIN
assets/icons/help_icon_dark_4x.png
Normal file
BIN
assets/icons/help_icon_dark_4x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -6,15 +6,15 @@ use nostrdb::Ndb;
|
|||||||
use crate::{
|
use crate::{
|
||||||
column::Columns,
|
column::Columns,
|
||||||
imgcache::ImageCache,
|
imgcache::ImageCache,
|
||||||
key_storage::{KeyStorage, KeyStorageResponse, KeyStorageType},
|
|
||||||
login_manager::LoginState,
|
login_manager::LoginState,
|
||||||
route::{Route, Router},
|
route::{Route, Router},
|
||||||
|
storage::{KeyStorageResponse, KeyStorageType},
|
||||||
ui::{
|
ui::{
|
||||||
account_login_view::{AccountLoginResponse, AccountLoginView},
|
account_login_view::{AccountLoginResponse, AccountLoginView},
|
||||||
account_management::{AccountsView, AccountsViewResponse},
|
account_management::{AccountsView, AccountsViewResponse},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tracing::info;
|
use tracing::{error, info};
|
||||||
|
|
||||||
pub use crate::user_account::UserAccount;
|
pub use crate::user_account::UserAccount;
|
||||||
|
|
||||||
@@ -96,13 +96,14 @@ pub fn process_accounts_view_response(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AccountManager {
|
impl AccountManager {
|
||||||
pub fn new(currently_selected_account: Option<usize>, key_store: KeyStorageType) -> Self {
|
pub fn new(key_store: KeyStorageType) -> Self {
|
||||||
let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() {
|
let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() {
|
||||||
res.unwrap_or_default()
|
res.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let currently_selected_account = get_selected_index(&accounts, &key_store);
|
||||||
AccountManager {
|
AccountManager {
|
||||||
currently_selected_account,
|
currently_selected_account,
|
||||||
accounts,
|
accounts,
|
||||||
@@ -188,16 +189,31 @@ impl AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_account(&mut self, index: usize) {
|
pub fn select_account(&mut self, index: usize) {
|
||||||
if self.accounts.get(index).is_some() {
|
if let Some(account) = self.accounts.get(index) {
|
||||||
self.currently_selected_account = Some(index)
|
self.currently_selected_account = Some(index);
|
||||||
|
self.key_store.select_key(Some(account.pubkey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_selected_account(&mut self) {
|
pub fn clear_selected_account(&mut self) {
|
||||||
self.currently_selected_account = None
|
self.currently_selected_account = None;
|
||||||
|
self.key_store.select_key(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_selected_index(accounts: &[UserAccount], keystore: &KeyStorageType) -> Option<usize> {
|
||||||
|
match keystore.get_selected_key() {
|
||||||
|
KeyStorageResponse::ReceivedResult(Ok(Some(pubkey))) => {
|
||||||
|
return accounts.iter().position(|account| account.pubkey == pubkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyStorageResponse::ReceivedResult(Err(e)) => error!("Error getting selected key: {}", e),
|
||||||
|
KeyStorageResponse::Waiting | KeyStorageResponse::ReceivedResult(Ok(None)) => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_login_view_response(manager: &mut AccountManager, response: AccountLoginResponse) {
|
pub fn process_login_view_response(manager: &mut AccountManager, response: AccountLoginResponse) {
|
||||||
match response {
|
match response {
|
||||||
AccountLoginResponse::CreateNew => {
|
AccountLoginResponse::CreateNew => {
|
||||||
@@ -207,4 +223,5 @@ pub fn process_login_view_response(manager: &mut AccountManager, response: Accou
|
|||||||
manager.add_account(keypair);
|
manager.add_account(keypair);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
manager.select_account(manager.num_accounts() - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/app.rs
53
src/app.rs
@@ -1,6 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
account_manager::AccountManager,
|
account_manager::AccountManager,
|
||||||
app_creation::setup_cc,
|
app_creation::setup_cc,
|
||||||
|
app_size_handler::AppSizeHandler,
|
||||||
app_style::user_requested_visuals_change,
|
app_style::user_requested_visuals_change,
|
||||||
args::Args,
|
args::Args,
|
||||||
column::Columns,
|
column::Columns,
|
||||||
@@ -9,19 +10,20 @@ use crate::{
|
|||||||
filter::{self, FilterState},
|
filter::{self, FilterState},
|
||||||
frame_history::FrameHistory,
|
frame_history::FrameHistory,
|
||||||
imgcache::ImageCache,
|
imgcache::ImageCache,
|
||||||
key_storage::KeyStorageType,
|
|
||||||
nav,
|
nav,
|
||||||
note::NoteRef,
|
note::NoteRef,
|
||||||
notecache::{CachedNote, NoteCache},
|
notecache::{CachedNote, NoteCache},
|
||||||
notes_holder::NotesHolderStorage,
|
notes_holder::NotesHolderStorage,
|
||||||
profile::Profile,
|
profile::Profile,
|
||||||
|
storage::{Directory, FileKeyStorage, KeyStorageType},
|
||||||
subscriptions::{SubKind, Subscriptions},
|
subscriptions::{SubKind, Subscriptions},
|
||||||
|
support::Support,
|
||||||
thread::Thread,
|
thread::Thread,
|
||||||
timeline::{Timeline, TimelineId, TimelineKind, ViewFilter},
|
timeline::{Timeline, TimelineId, TimelineKind, ViewFilter},
|
||||||
ui::{self, DesktopSidePanel},
|
ui::{self, DesktopSidePanel},
|
||||||
unknowns::UnknownIds,
|
unknowns::UnknownIds,
|
||||||
view_state::ViewState,
|
view_state::ViewState,
|
||||||
Result,
|
DataPaths, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use enostr::{ClientMessage, RelayEvent, RelayMessage, RelayPool};
|
use enostr::{ClientMessage, RelayEvent, RelayMessage, RelayPool};
|
||||||
@@ -60,6 +62,8 @@ pub struct Damus {
|
|||||||
pub img_cache: ImageCache,
|
pub img_cache: ImageCache,
|
||||||
pub accounts: AccountManager,
|
pub accounts: AccountManager,
|
||||||
pub subscriptions: Subscriptions,
|
pub subscriptions: Subscriptions,
|
||||||
|
pub app_rect_handler: AppSizeHandler,
|
||||||
|
pub support: Support,
|
||||||
|
|
||||||
frame_history: crate::frame_history::FrameHistory,
|
frame_history: crate::frame_history::FrameHistory,
|
||||||
|
|
||||||
@@ -507,6 +511,8 @@ fn update_damus(damus: &mut Damus, ctx: &egui::Context) {
|
|||||||
error!("error processing event: {}", err);
|
error!("error processing event: {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
damus.app_rect_handler.try_save_app_size(ctx);
|
||||||
|
|
||||||
damus.columns.attempt_perform_deletion_request();
|
damus.columns.attempt_perform_deletion_request();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,20 +670,35 @@ impl Damus {
|
|||||||
let mut config = Config::new();
|
let mut config = Config::new();
|
||||||
config.set_ingester_threads(4);
|
config.set_ingester_threads(4);
|
||||||
|
|
||||||
let mut accounts = AccountManager::new(
|
let keystore = if parsed_args.use_keystore {
|
||||||
// TODO: should pull this from settings
|
if let Ok(keys_path) = DataPaths::Keys.get_path() {
|
||||||
None,
|
if let Ok(selected_key_path) = DataPaths::SelectedKey.get_path() {
|
||||||
// TODO: use correct KeyStorage mechanism for current OS arch
|
KeyStorageType::FileSystem(FileKeyStorage::new(
|
||||||
KeyStorageType::None,
|
Directory::new(keys_path),
|
||||||
);
|
Directory::new(selected_key_path),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
error!("Could not find path for selected key");
|
||||||
|
KeyStorageType::None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("Could not find data path for keys");
|
||||||
|
KeyStorageType::None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
KeyStorageType::None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut accounts = AccountManager::new(keystore);
|
||||||
|
|
||||||
|
let num_keys = parsed_args.keys.len();
|
||||||
|
|
||||||
for key in parsed_args.keys {
|
for key in parsed_args.keys {
|
||||||
info!("adding account: {}", key.pubkey);
|
info!("adding account: {}", key.pubkey);
|
||||||
accounts.add_account(key);
|
accounts.add_account(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: pull currently selected account from settings
|
if num_keys != 0 {
|
||||||
if accounts.num_accounts() > 0 {
|
|
||||||
accounts.select_account(0);
|
accounts.select_account(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,6 +758,8 @@ impl Damus {
|
|||||||
accounts,
|
accounts,
|
||||||
frame_history: FrameHistory::default(),
|
frame_history: FrameHistory::default(),
|
||||||
view_state: ViewState::default(),
|
view_state: ViewState::default(),
|
||||||
|
app_rect_handler: AppSizeHandler::default(),
|
||||||
|
support: Support::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,9 +840,11 @@ impl Damus {
|
|||||||
columns,
|
columns,
|
||||||
textmode: false,
|
textmode: false,
|
||||||
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
|
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
|
||||||
accounts: AccountManager::new(None, KeyStorageType::None),
|
accounts: AccountManager::new(KeyStorageType::None),
|
||||||
frame_history: FrameHistory::default(),
|
frame_history: FrameHistory::default(),
|
||||||
view_state: ViewState::default(),
|
view_state: ViewState::default(),
|
||||||
|
app_rect_handler: AppSizeHandler::default(),
|
||||||
|
support: Support::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,7 +1034,11 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) {
|
|||||||
.show(ui);
|
.show(ui);
|
||||||
|
|
||||||
if side_panel.response.clicked() {
|
if side_panel.response.clicked() {
|
||||||
DesktopSidePanel::perform_action(app.columns_mut(), side_panel.action);
|
DesktopSidePanel::perform_action(
|
||||||
|
&mut app.columns,
|
||||||
|
&mut app.support,
|
||||||
|
side_panel.action,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// vertical sidebar line
|
// vertical sidebar line
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::app_size_handler::AppSizeHandler;
|
||||||
use crate::app_style::{
|
use crate::app_style::{
|
||||||
create_custom_style, dark_mode, desktop_font_size, light_mode, mobile_font_size,
|
create_custom_style, dark_mode, desktop_font_size, light_mode, mobile_font_size,
|
||||||
};
|
};
|
||||||
@@ -8,10 +9,16 @@ use eframe::NativeOptions;
|
|||||||
|
|
||||||
pub fn generate_native_options() -> NativeOptions {
|
pub fn generate_native_options() -> NativeOptions {
|
||||||
generate_native_options_with_builder_modifiers(|builder| {
|
generate_native_options_with_builder_modifiers(|builder| {
|
||||||
builder
|
let builder = builder
|
||||||
.with_fullsize_content_view(true)
|
.with_fullsize_content_view(true)
|
||||||
.with_titlebar_shown(false)
|
.with_titlebar_shown(false)
|
||||||
.with_title_shown(false)
|
.with_title_shown(false);
|
||||||
|
|
||||||
|
if let Some(window_size) = AppSizeHandler::default().get_app_size() {
|
||||||
|
builder.with_inner_size(window_size)
|
||||||
|
} else {
|
||||||
|
builder
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
100
src/app_size_handler.rs
Normal file
100
src/app_size_handler.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use egui::Context;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
storage::{write_file, Directory},
|
||||||
|
DataPaths,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AppSizeHandler {
|
||||||
|
directory: Option<Directory>,
|
||||||
|
saved_size: Option<egui::Vec2>,
|
||||||
|
last_saved: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
static FILE_NAME: &str = "app_size.json";
|
||||||
|
static DELAY: Duration = Duration::from_millis(500);
|
||||||
|
|
||||||
|
impl Default for AppSizeHandler {
|
||||||
|
fn default() -> Self {
|
||||||
|
let directory = match DataPaths::Setting.get_path() {
|
||||||
|
Ok(path) => Some(Directory::new(path)),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Could not load settings path: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
directory,
|
||||||
|
saved_size: None,
|
||||||
|
last_saved: Instant::now() - DELAY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppSizeHandler {
|
||||||
|
pub fn try_save_app_size(&mut self, ctx: &Context) {
|
||||||
|
if let Some(interactor) = &self.directory {
|
||||||
|
// There doesn't seem to be a way to check if user is resizing window, so if the rect is different than last saved, we'll wait DELAY before saving again to avoid spamming io
|
||||||
|
if self.last_saved.elapsed() >= DELAY {
|
||||||
|
internal_try_save_app_size(interactor, &mut self.saved_size, ctx);
|
||||||
|
self.last_saved = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_app_size(&self) -> Option<egui::Vec2> {
|
||||||
|
if self.saved_size.is_some() {
|
||||||
|
return self.saved_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(directory) = &self.directory {
|
||||||
|
if let Ok(file_contents) = directory.get_file(FILE_NAME.to_owned()) {
|
||||||
|
if let Ok(rect) = serde_json::from_str::<egui::Vec2>(&file_contents) {
|
||||||
|
return Some(rect);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Could not find {}", FILE_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn internal_try_save_app_size(
|
||||||
|
interactor: &Directory,
|
||||||
|
maybe_saved_size: &mut Option<egui::Vec2>,
|
||||||
|
ctx: &Context,
|
||||||
|
) {
|
||||||
|
let cur_size = ctx.input(|i| i.screen_rect.size());
|
||||||
|
if let Some(saved_size) = maybe_saved_size {
|
||||||
|
if cur_size != *saved_size {
|
||||||
|
try_save_size(interactor, cur_size, maybe_saved_size);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try_save_size(interactor, cur_size, maybe_saved_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_save_size(
|
||||||
|
interactor: &Directory,
|
||||||
|
cur_size: egui::Vec2,
|
||||||
|
maybe_saved_size: &mut Option<egui::Vec2>,
|
||||||
|
) {
|
||||||
|
if let Ok(serialized_rect) = serde_json::to_string(&cur_size) {
|
||||||
|
if write_file(
|
||||||
|
&interactor.file_path,
|
||||||
|
FILE_NAME.to_owned(),
|
||||||
|
&serialized_rect,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
info!("wrote size {}", cur_size,);
|
||||||
|
*maybe_saved_size = Some(cur_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ pub struct Args {
|
|||||||
pub light: bool,
|
pub light: bool,
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
pub textmode: bool,
|
pub textmode: bool,
|
||||||
|
pub use_keystore: bool,
|
||||||
pub dbpath: Option<String>,
|
pub dbpath: Option<String>,
|
||||||
pub datapath: Option<String>,
|
pub datapath: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -28,6 +29,7 @@ impl Args {
|
|||||||
since_optimize: true,
|
since_optimize: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
textmode: false,
|
textmode: false,
|
||||||
|
use_keystore: true,
|
||||||
dbpath: None,
|
dbpath: None,
|
||||||
datapath: None,
|
datapath: None,
|
||||||
};
|
};
|
||||||
@@ -210,6 +212,8 @@ impl Args {
|
|||||||
} else {
|
} else {
|
||||||
error!("failed to parse filter in '{}'", filter_file);
|
error!("failed to parse filter in '{}'", filter_file);
|
||||||
}
|
}
|
||||||
|
} else if arg == "--no-keystore" {
|
||||||
|
res.use_keystore = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
i += 1;
|
i += 1;
|
||||||
|
|||||||
@@ -11,8 +11,57 @@ use notedeck::Damus;
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
#[allow(unused_variables)] // need guard to live for lifetime of program
|
||||||
|
let (maybe_non_blocking, maybe_guard) =
|
||||||
|
if let Ok(log_path) = notedeck::DataPaths::Log.get_path() {
|
||||||
|
// Setup logging to file
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
use tracing::error;
|
||||||
|
use tracing_appender::{
|
||||||
|
non_blocking,
|
||||||
|
rolling::{RollingFileAppender, Rotation},
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_appender = RollingFileAppender::new(
|
||||||
|
Rotation::DAILY,
|
||||||
|
log_path,
|
||||||
|
format!("notedeck-{}.log", env!("CARGO_PKG_VERSION")),
|
||||||
|
);
|
||||||
|
panic::set_hook(Box::new(|panic_info| {
|
||||||
|
error!("Notedeck panicked: {:?}", panic_info);
|
||||||
|
}));
|
||||||
|
|
||||||
|
let (non_blocking, _guard) = non_blocking(file_appender);
|
||||||
|
|
||||||
|
(Some(non_blocking), Some(_guard))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||||
tracing_subscriber::fmt::init();
|
if let Some(non_blocking_writer) = maybe_non_blocking {
|
||||||
|
use tracing::Level;
|
||||||
|
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
let console_layer = fmt::layer().with_target(true).with_writer(std::io::stdout);
|
||||||
|
|
||||||
|
// Create the file layer (writes to the file)
|
||||||
|
let file_layer = fmt::layer()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_writer(non_blocking_writer);
|
||||||
|
|
||||||
|
// Set up the subscriber to combine both layers
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(console_layer)
|
||||||
|
.with(file_layer)
|
||||||
|
.with(tracing_subscriber::filter::LevelFilter::from_level(
|
||||||
|
Level::INFO,
|
||||||
|
)) // Set log level
|
||||||
|
.init();
|
||||||
|
} else {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
}
|
||||||
|
|
||||||
let _res = eframe::run_native(
|
let _res = eframe::run_native(
|
||||||
"Damus NoteDeck",
|
"Damus NoteDeck",
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
use enostr::Keypair;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use crate::linux_key_storage::LinuxKeyStorage;
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use crate::macos_key_storage::MacOSKeyStorage;
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub const SERVICE_NAME: &str = "Notedeck";
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub enum KeyStorageType {
|
|
||||||
None,
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
MacOS,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
Linux,
|
|
||||||
// TODO:
|
|
||||||
// Windows,
|
|
||||||
// Android,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub enum KeyStorageResponse<R> {
|
|
||||||
Waiting,
|
|
||||||
ReceivedResult(Result<R, KeyStorageError>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait KeyStorage {
|
|
||||||
fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>>;
|
|
||||||
fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()>;
|
|
||||||
fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyStorage for KeyStorageType {
|
|
||||||
fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
|
|
||||||
match self {
|
|
||||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(Vec::new())),
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).get_keys(),
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
Self::Linux => LinuxKeyStorage::new().get_keys(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
|
||||||
let _ = key;
|
|
||||||
match self {
|
|
||||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).add_key(key),
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
Self::Linux => LinuxKeyStorage::new().add_key(key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
|
||||||
let _ = key;
|
|
||||||
match self {
|
|
||||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
Self::MacOS => MacOSKeyStorage::new(SERVICE_NAME).remove_key(key),
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
Self::Linux => LinuxKeyStorage::new().remove_key(key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub enum KeyStorageError {
|
|
||||||
Retrieval(String),
|
|
||||||
Addition(String),
|
|
||||||
Removal(String),
|
|
||||||
OSError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for KeyStorageError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Retrieval(e) => write!(f, "Failed to retrieve keys: {:?}", e),
|
|
||||||
Self::Addition(key) => write!(f, "Failed to add key: {:?}", key),
|
|
||||||
Self::Removal(key) => write!(f, "Failed to remove key: {:?}", key),
|
|
||||||
Self::OSError(e) => write!(f, "OS had an error: {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for KeyStorageError {}
|
|
||||||
@@ -7,6 +7,7 @@ mod abbrev;
|
|||||||
pub mod account_manager;
|
pub mod account_manager;
|
||||||
mod actionbar;
|
mod actionbar;
|
||||||
pub mod app_creation;
|
pub mod app_creation;
|
||||||
|
mod app_size_handler;
|
||||||
mod app_style;
|
mod app_style;
|
||||||
mod args;
|
mod args;
|
||||||
mod colors;
|
mod colors;
|
||||||
@@ -18,9 +19,7 @@ mod frame_history;
|
|||||||
mod images;
|
mod images;
|
||||||
mod imgcache;
|
mod imgcache;
|
||||||
mod key_parsing;
|
mod key_parsing;
|
||||||
mod key_storage;
|
|
||||||
pub mod login_manager;
|
pub mod login_manager;
|
||||||
mod macos_key_storage;
|
|
||||||
mod multi_subscriber;
|
mod multi_subscriber;
|
||||||
mod nav;
|
mod nav;
|
||||||
mod note;
|
mod note;
|
||||||
@@ -32,6 +31,7 @@ pub mod relay_pool_manager;
|
|||||||
mod result;
|
mod result;
|
||||||
mod route;
|
mod route;
|
||||||
mod subscriptions;
|
mod subscriptions;
|
||||||
|
mod support;
|
||||||
mod test_data;
|
mod test_data;
|
||||||
mod thread;
|
mod thread;
|
||||||
mod time;
|
mod time;
|
||||||
@@ -45,11 +45,13 @@ mod view_state;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod test_utils;
|
mod test_utils;
|
||||||
mod linux_key_storage;
|
|
||||||
|
mod storage;
|
||||||
|
|
||||||
pub use app::Damus;
|
pub use app::Damus;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use profile::DisplayName;
|
pub use profile::DisplayName;
|
||||||
|
pub use storage::DataPaths;
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
use winit::platform::android::EventLoopBuilderExtAndroid;
|
use winit::platform::android::EventLoopBuilderExtAndroid;
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
#![cfg(target_os = "linux")]
|
|
||||||
|
|
||||||
use enostr::{Keypair, SerializableKeypair};
|
|
||||||
use std::fs;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::{env, fs::File};
|
|
||||||
|
|
||||||
use crate::key_storage::{KeyStorage, KeyStorageError, KeyStorageResponse};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
enum LinuxKeyStorageType {
|
|
||||||
BasicFileStorage,
|
|
||||||
// TODO(kernelkind): could use the secret service api, and maybe even allow password manager integration via a settings menu
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LinuxKeyStorage {}
|
|
||||||
|
|
||||||
// TODO(kernelkind): read from settings instead of hard-coding
|
|
||||||
static USE_MECHANISM: LinuxKeyStorageType = LinuxKeyStorageType::BasicFileStorage;
|
|
||||||
|
|
||||||
impl LinuxKeyStorage {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyStorage for LinuxKeyStorage {
|
|
||||||
fn get_keys(&self) -> KeyStorageResponse<Vec<enostr::Keypair>> {
|
|
||||||
match USE_MECHANISM {
|
|
||||||
LinuxKeyStorageType::BasicFileStorage => BasicFileStorage::new().get_keys(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
|
|
||||||
match USE_MECHANISM {
|
|
||||||
LinuxKeyStorageType::BasicFileStorage => BasicFileStorage::new().add_key(key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
|
|
||||||
match USE_MECHANISM {
|
|
||||||
LinuxKeyStorageType::BasicFileStorage => BasicFileStorage::new().remove_key(key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct BasicFileStorage {
|
|
||||||
credential_dir_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BasicFileStorage {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
credential_dir_name: ".credentials".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mock() -> Self {
|
|
||||||
Self {
|
|
||||||
credential_dir_name: ".credentials_test".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cred_dirpath(&self) -> Result<PathBuf, KeyStorageError> {
|
|
||||||
let home_dir = env::var("HOME")
|
|
||||||
.map_err(|_| KeyStorageError::OSError("HOME env variable not set".to_string()))?;
|
|
||||||
let home_path = std::path::PathBuf::from(home_dir);
|
|
||||||
let project_path_str = "notedeck";
|
|
||||||
|
|
||||||
let config_path = {
|
|
||||||
if let Some(xdg_config_str) = env::var_os("XDG_CONFIG_HOME") {
|
|
||||||
let xdg_path = PathBuf::from(xdg_config_str);
|
|
||||||
let xdg_path_config = if xdg_path.is_absolute() {
|
|
||||||
xdg_path
|
|
||||||
} else {
|
|
||||||
home_path.join(".config")
|
|
||||||
};
|
|
||||||
xdg_path_config.join(project_path_str)
|
|
||||||
} else {
|
|
||||||
home_path.join(format!(".{}", project_path_str))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.join(self.credential_dir_name.clone());
|
|
||||||
|
|
||||||
std::fs::create_dir_all(&config_path).map_err(|_| {
|
|
||||||
KeyStorageError::OSError(format!(
|
|
||||||
"could not create config path: {}",
|
|
||||||
config_path.display()
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(config_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
|
||||||
let mut file_path = self.get_cred_dirpath()?;
|
|
||||||
file_path.push(format!("{}", &key.pubkey));
|
|
||||||
|
|
||||||
let mut file = File::create(file_path)
|
|
||||||
.map_err(|_| KeyStorageError::Addition("could not create or open file".to_string()))?;
|
|
||||||
|
|
||||||
let json_str = serde_json::to_string(&SerializableKeypair::from_keypair(key, "", 7))
|
|
||||||
.map_err(|e| KeyStorageError::Addition(e.to_string()))?;
|
|
||||||
file.write_all(json_str.as_bytes()).map_err(|_| {
|
|
||||||
KeyStorageError::Addition("could not write keypair to file".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_keys_internal(&self) -> Result<Vec<Keypair>, KeyStorageError> {
|
|
||||||
let file_path = self.get_cred_dirpath()?;
|
|
||||||
let mut keys: Vec<Keypair> = Vec::new();
|
|
||||||
|
|
||||||
if !file_path.is_dir() {
|
|
||||||
return Err(KeyStorageError::Retrieval(
|
|
||||||
"path is not a directory".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let dir = fs::read_dir(file_path).map_err(|_| {
|
|
||||||
KeyStorageError::Retrieval("problem accessing credentials directory".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for entry in dir {
|
|
||||||
let entry = entry.map_err(|_| {
|
|
||||||
KeyStorageError::Retrieval("problem accessing crediential file".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
if path.is_file() {
|
|
||||||
if let Some(path_str) = path.to_str() {
|
|
||||||
debug!("key path {}", path_str);
|
|
||||||
let json_string = fs::read_to_string(path_str).map_err(|e| {
|
|
||||||
KeyStorageError::OSError(format!("File reading problem: {}", e))
|
|
||||||
})?;
|
|
||||||
let key: SerializableKeypair =
|
|
||||||
serde_json::from_str(&json_string).map_err(|e| {
|
|
||||||
KeyStorageError::OSError(format!(
|
|
||||||
"Deserialization problem: {}",
|
|
||||||
(e.to_string().as_str())
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
keys.push(key.to_keypair(""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
|
||||||
let path = self.get_cred_dirpath()?;
|
|
||||||
|
|
||||||
let filepath = path.join(key.pubkey.to_string());
|
|
||||||
|
|
||||||
if filepath.exists() && filepath.is_file() {
|
|
||||||
fs::remove_file(&filepath)
|
|
||||||
.map_err(|e| KeyStorageError::OSError(format!("failed to remove file: {}", e)))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyStorage for BasicFileStorage {
|
|
||||||
fn get_keys(&self) -> crate::key_storage::KeyStorageResponse<Vec<enostr::Keypair>> {
|
|
||||||
KeyStorageResponse::ReceivedResult(self.get_keys_internal())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_key(&self, key: &enostr::Keypair) -> crate::key_storage::KeyStorageResponse<()> {
|
|
||||||
KeyStorageResponse::ReceivedResult(self.add_key_internal(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_key(&self, key: &enostr::Keypair) -> crate::key_storage::KeyStorageResponse<()> {
|
|
||||||
KeyStorageResponse::ReceivedResult(self.remove_key_internal(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod tests {
|
|
||||||
use crate::key_storage::{KeyStorage, KeyStorageResponse};
|
|
||||||
|
|
||||||
use super::BasicFileStorage;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic() {
|
|
||||||
let kp = enostr::FullKeypair::generate().to_keypair();
|
|
||||||
let resp = BasicFileStorage::mock().add_key(&kp);
|
|
||||||
|
|
||||||
assert_eq!(resp, KeyStorageResponse::ReceivedResult(Ok(())));
|
|
||||||
assert_num_storage(1);
|
|
||||||
|
|
||||||
let resp = BasicFileStorage::mock().remove_key(&kp);
|
|
||||||
assert_eq!(resp, KeyStorageResponse::ReceivedResult(Ok(())));
|
|
||||||
assert_num_storage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn assert_num_storage(n: usize) {
|
|
||||||
let resp = BasicFileStorage::mock().get_keys();
|
|
||||||
|
|
||||||
if let KeyStorageResponse::ReceivedResult(Ok(vec)) = resp {
|
|
||||||
assert_eq!(vec.len(), n);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
panic!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@ use crate::{
|
|||||||
add_column::{AddColumnResponse, AddColumnView},
|
add_column::{AddColumnResponse, AddColumnView},
|
||||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||||
note::PostAction,
|
note::PostAction,
|
||||||
|
support::SupportView,
|
||||||
RelayView, View,
|
RelayView, View,
|
||||||
},
|
},
|
||||||
Damus,
|
Damus,
|
||||||
@@ -128,6 +129,10 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) {
|
|||||||
col,
|
col,
|
||||||
ui,
|
ui,
|
||||||
),
|
),
|
||||||
|
Route::Support => {
|
||||||
|
SupportView::new(&mut app.support).show(ui);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub enum Route {
|
|||||||
ComposeNote,
|
ComposeNote,
|
||||||
AddColumn,
|
AddColumn,
|
||||||
Profile(Pubkey),
|
Profile(Pubkey),
|
||||||
|
Support,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -100,6 +101,7 @@ impl Route {
|
|||||||
Route::Profile(pubkey) => {
|
Route::Profile(pubkey) => {
|
||||||
format!("{}'s Profile", get_profile_displayname_string(ndb, pubkey))
|
format!("{}'s Profile", get_profile_displayname_string(ndb, pubkey))
|
||||||
}
|
}
|
||||||
|
Route::Support => "Damus Support".to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
TitledRoute {
|
TitledRoute {
|
||||||
@@ -208,6 +210,7 @@ impl fmt::Display for Route {
|
|||||||
|
|
||||||
Route::AddColumn => write!(f, "Add Column"),
|
Route::AddColumn => write!(f, "Add Column"),
|
||||||
Route::Profile(_) => write!(f, "Profile"),
|
Route::Profile(_) => write!(f, "Profile"),
|
||||||
|
Route::Support => write!(f, "Support"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
176
src/storage/file_key_storage.rs
Normal file
176
src/storage/file_key_storage.rs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
use eframe::Result;
|
||||||
|
use enostr::{Keypair, Pubkey, SerializableKeypair};
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
file_storage::{delete_file, write_file, Directory},
|
||||||
|
key_storage_impl::{KeyStorageError, KeyStorageResponse},
|
||||||
|
};
|
||||||
|
|
||||||
|
static SELECTED_PUBKEY_FILE_NAME: &str = "selected_pubkey";
|
||||||
|
|
||||||
|
/// An OS agnostic file key storage implementation
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct FileKeyStorage {
|
||||||
|
keys_directory: Directory,
|
||||||
|
selected_key_directory: Directory,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileKeyStorage {
|
||||||
|
pub fn new(keys_directory: Directory, selected_key_directory: Directory) -> Self {
|
||||||
|
Self {
|
||||||
|
keys_directory,
|
||||||
|
selected_key_directory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
||||||
|
write_file(
|
||||||
|
&self.keys_directory.file_path,
|
||||||
|
key.pubkey.hex(),
|
||||||
|
&serde_json::to_string(&SerializableKeypair::from_keypair(key, "", 7))
|
||||||
|
.map_err(|e| KeyStorageError::Addition(Error::Generic(e.to_string())))?,
|
||||||
|
)
|
||||||
|
.map_err(KeyStorageError::Addition)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_keys_internal(&self) -> Result<Vec<Keypair>, KeyStorageError> {
|
||||||
|
let keys = self
|
||||||
|
.keys_directory
|
||||||
|
.get_files()
|
||||||
|
.map_err(KeyStorageError::Retrieval)?
|
||||||
|
.values()
|
||||||
|
.filter_map(|str_key| serde_json::from_str::<SerializableKeypair>(str_key).ok())
|
||||||
|
.map(|serializable_keypair| serializable_keypair.to_keypair(""))
|
||||||
|
.collect();
|
||||||
|
Ok(keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
||||||
|
delete_file(&self.keys_directory.file_path, key.pubkey.hex())
|
||||||
|
.map_err(KeyStorageError::Removal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_selected_pubkey(&self) -> Result<Option<Pubkey>, KeyStorageError> {
|
||||||
|
let pubkey_str = self
|
||||||
|
.selected_key_directory
|
||||||
|
.get_file(SELECTED_PUBKEY_FILE_NAME.to_owned())
|
||||||
|
.map_err(KeyStorageError::Selection)?;
|
||||||
|
|
||||||
|
serde_json::from_str(&pubkey_str)
|
||||||
|
.map_err(|e| KeyStorageError::Selection(Error::Generic(e.to_string())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_pubkey(&self, pubkey: Option<Pubkey>) -> Result<(), KeyStorageError> {
|
||||||
|
if let Some(pubkey) = pubkey {
|
||||||
|
write_file(
|
||||||
|
&self.selected_key_directory.file_path,
|
||||||
|
SELECTED_PUBKEY_FILE_NAME.to_owned(),
|
||||||
|
&serde_json::to_string(&pubkey.hex())
|
||||||
|
.map_err(|e| KeyStorageError::Selection(Error::Generic(e.to_string())))?,
|
||||||
|
)
|
||||||
|
.map_err(KeyStorageError::Selection)
|
||||||
|
} else if self
|
||||||
|
.selected_key_directory
|
||||||
|
.get_file(SELECTED_PUBKEY_FILE_NAME.to_owned())
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
// Case where user chose to have no selected pubkey, but one already exists
|
||||||
|
delete_file(
|
||||||
|
&self.selected_key_directory.file_path,
|
||||||
|
SELECTED_PUBKEY_FILE_NAME.to_owned(),
|
||||||
|
)
|
||||||
|
.map_err(KeyStorageError::Selection)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileKeyStorage {
|
||||||
|
pub fn get_keys(&self) -> KeyStorageResponse<Vec<enostr::Keypair>> {
|
||||||
|
KeyStorageResponse::ReceivedResult(self.get_keys_internal())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
|
||||||
|
KeyStorageResponse::ReceivedResult(self.add_key_internal(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
|
||||||
|
KeyStorageResponse::ReceivedResult(self.remove_key_internal(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_key(&self) -> KeyStorageResponse<Option<Pubkey>> {
|
||||||
|
KeyStorageResponse::ReceivedResult(self.get_selected_pubkey())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_key(&self, key: Option<Pubkey>) -> KeyStorageResponse<()> {
|
||||||
|
KeyStorageResponse::ReceivedResult(self.select_pubkey(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use enostr::Keypair;
|
||||||
|
static CREATE_TMP_DIR: fn() -> Result<PathBuf, Error> =
|
||||||
|
|| Ok(tempfile::TempDir::new()?.path().to_path_buf());
|
||||||
|
|
||||||
|
impl FileKeyStorage {
|
||||||
|
fn mock() -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
keys_directory: Directory::new(CREATE_TMP_DIR()?),
|
||||||
|
selected_key_directory: Directory::new(CREATE_TMP_DIR()?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic() {
|
||||||
|
let kp = enostr::FullKeypair::generate().to_keypair();
|
||||||
|
let storage = FileKeyStorage::mock().unwrap();
|
||||||
|
let resp = storage.add_key(&kp);
|
||||||
|
|
||||||
|
assert_eq!(resp, KeyStorageResponse::ReceivedResult(Ok(())));
|
||||||
|
assert_num_storage(&storage.get_keys(), 1);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
storage.remove_key(&kp),
|
||||||
|
KeyStorageResponse::ReceivedResult(Ok(()))
|
||||||
|
);
|
||||||
|
assert_num_storage(&storage.get_keys(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_num_storage(keys_response: &KeyStorageResponse<Vec<Keypair>>, n: usize) {
|
||||||
|
match keys_response {
|
||||||
|
KeyStorageResponse::ReceivedResult(Ok(keys)) => {
|
||||||
|
assert_eq!(keys.len(), n);
|
||||||
|
}
|
||||||
|
KeyStorageResponse::ReceivedResult(Err(_e)) => {
|
||||||
|
panic!("could not get keys");
|
||||||
|
}
|
||||||
|
KeyStorageResponse::Waiting => {
|
||||||
|
panic!("did not receive result");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_key() {
|
||||||
|
let kp = enostr::FullKeypair::generate().to_keypair();
|
||||||
|
|
||||||
|
let storage = FileKeyStorage::mock().unwrap();
|
||||||
|
let _ = storage.add_key(&kp);
|
||||||
|
assert_num_storage(&storage.get_keys(), 1);
|
||||||
|
|
||||||
|
let resp = storage.select_pubkey(Some(kp.pubkey));
|
||||||
|
assert!(resp.is_ok());
|
||||||
|
|
||||||
|
let resp = storage.get_selected_pubkey();
|
||||||
|
|
||||||
|
assert!(resp.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
259
src/storage/file_storage.rs
Normal file
259
src/storage/file_storage.rs
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{HashMap, VecDeque},
|
||||||
|
fs::{self, File},
|
||||||
|
io::{self, BufRead},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
pub enum DataPaths {
|
||||||
|
Log,
|
||||||
|
Setting,
|
||||||
|
Keys,
|
||||||
|
SelectedKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataPaths {
|
||||||
|
pub fn get_path(&self) -> Result<PathBuf, Error> {
|
||||||
|
let base_path = match self {
|
||||||
|
DataPaths::Log => dirs::data_local_dir(),
|
||||||
|
DataPaths::Setting | DataPaths::Keys | DataPaths::SelectedKey => {
|
||||||
|
dirs::config_local_dir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ok_or(Error::Generic(
|
||||||
|
"Could not open well known OS directory".to_owned(),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let specific_path = match self {
|
||||||
|
DataPaths::Log => PathBuf::from("logs"),
|
||||||
|
DataPaths::Setting => PathBuf::from("settings"),
|
||||||
|
DataPaths::Keys => PathBuf::from("storage").join("accounts"),
|
||||||
|
DataPaths::SelectedKey => PathBuf::from("storage").join("selected_account"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(base_path.join("notedeck").join(specific_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct Directory {
|
||||||
|
pub file_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Directory {
|
||||||
|
pub fn new(file_path: PathBuf) -> Self {
|
||||||
|
Self { file_path }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the files in the current directory where the key is the file name and the value is the file contents
|
||||||
|
pub fn get_files(&self) -> Result<HashMap<String, String>, Error> {
|
||||||
|
let dir = fs::read_dir(self.file_path.clone())?;
|
||||||
|
let map = dir
|
||||||
|
.filter_map(|f| f.ok())
|
||||||
|
.filter(|f| f.path().is_file())
|
||||||
|
.filter_map(|f| {
|
||||||
|
let file_name = f.file_name().into_string().ok()?;
|
||||||
|
let contents = fs::read_to_string(f.path()).ok()?;
|
||||||
|
Some((file_name, contents))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_file_names(&self) -> Result<Vec<String>, Error> {
|
||||||
|
let dir = fs::read_dir(self.file_path.clone())?;
|
||||||
|
let names = dir
|
||||||
|
.filter_map(|f| f.ok())
|
||||||
|
.filter(|f| f.path().is_file())
|
||||||
|
.filter_map(|f| f.file_name().into_string().ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_file(&self, file_name: String) -> Result<String, Error> {
|
||||||
|
let filepath = self.file_path.clone().join(file_name.clone());
|
||||||
|
|
||||||
|
if filepath.exists() && filepath.is_file() {
|
||||||
|
let filepath_str = filepath
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| Error::Generic("Could not turn path to string".to_owned()))?;
|
||||||
|
Ok(fs::read_to_string(filepath_str)?)
|
||||||
|
} else {
|
||||||
|
Err(Error::Generic(format!(
|
||||||
|
"Requested file was not found: {}",
|
||||||
|
file_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_file_last_n_lines(&self, file_name: String, n: usize) -> Result<FileResult, Error> {
|
||||||
|
let filepath = self.file_path.clone().join(file_name.clone());
|
||||||
|
|
||||||
|
if filepath.exists() && filepath.is_file() {
|
||||||
|
let file = File::open(&filepath)?;
|
||||||
|
let reader = io::BufReader::new(file);
|
||||||
|
|
||||||
|
let mut queue: VecDeque<String> = VecDeque::with_capacity(n);
|
||||||
|
|
||||||
|
let mut total_lines_in_file = 0;
|
||||||
|
for line in reader.lines() {
|
||||||
|
let line = line?;
|
||||||
|
|
||||||
|
queue.push_back(line);
|
||||||
|
|
||||||
|
if queue.len() > n {
|
||||||
|
queue.pop_front();
|
||||||
|
}
|
||||||
|
total_lines_in_file += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_num_lines = queue.len();
|
||||||
|
let output = queue.into_iter().collect::<Vec<String>>().join("\n");
|
||||||
|
Ok(FileResult {
|
||||||
|
output,
|
||||||
|
output_num_lines,
|
||||||
|
total_lines_in_file,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(Error::Generic(format!(
|
||||||
|
"Requested file was not found: {}",
|
||||||
|
file_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the file name which is most recently modified in the directory
|
||||||
|
pub fn get_most_recent(&self) -> Result<Option<String>, Error> {
|
||||||
|
let mut most_recent: Option<(SystemTime, String)> = None;
|
||||||
|
|
||||||
|
for entry in fs::read_dir(&self.file_path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let metadata = entry.metadata()?;
|
||||||
|
if metadata.is_file() {
|
||||||
|
let modified = metadata.modified()?;
|
||||||
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
match most_recent {
|
||||||
|
Some((last_modified, _)) if modified > last_modified => {
|
||||||
|
most_recent = Some((modified, file_name));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
most_recent = Some((modified, file_name));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(most_recent.map(|(_, file_name)| file_name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileResult {
|
||||||
|
pub output: String,
|
||||||
|
pub output_num_lines: usize,
|
||||||
|
pub total_lines_in_file: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the file to the directory
|
||||||
|
pub fn write_file(directory: &Path, file_name: String, data: &str) -> Result<(), Error> {
|
||||||
|
if !directory.exists() {
|
||||||
|
fs::create_dir_all(directory)?
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(directory.join(file_name), data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_file(directory: &Path, file_name: String) -> Result<(), Error> {
|
||||||
|
let file_to_delete = directory.join(file_name.clone());
|
||||||
|
if file_to_delete.exists() && file_to_delete.is_file() {
|
||||||
|
fs::remove_file(file_to_delete).map_err(Error::Io)
|
||||||
|
} else {
|
||||||
|
Err(Error::Generic(format!(
|
||||||
|
"Requested file to delete was not found: {}",
|
||||||
|
file_name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
storage::file_storage::{delete_file, write_file},
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Directory;
|
||||||
|
|
||||||
|
static CREATE_TMP_DIR: fn() -> Result<PathBuf, Error> =
|
||||||
|
|| Ok(tempfile::TempDir::new()?.path().to_path_buf());
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_get_delete() {
|
||||||
|
if let Ok(path) = CREATE_TMP_DIR() {
|
||||||
|
let directory = Directory::new(path);
|
||||||
|
let file_name = "file_test_name.txt".to_string();
|
||||||
|
let file_contents = "test";
|
||||||
|
let write_res = write_file(&directory.file_path, file_name.clone(), file_contents);
|
||||||
|
assert!(write_res.is_ok());
|
||||||
|
|
||||||
|
if let Ok(asserted_file_contents) = directory.get_file(file_name.clone()) {
|
||||||
|
assert_eq!(asserted_file_contents, file_contents);
|
||||||
|
} else {
|
||||||
|
panic!("File not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let delete_res = delete_file(&directory.file_path, file_name);
|
||||||
|
assert!(delete_res.is_ok());
|
||||||
|
} else {
|
||||||
|
panic!("could not get interactor")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_multiple() {
|
||||||
|
if let Ok(path) = CREATE_TMP_DIR() {
|
||||||
|
let directory = Directory::new(path);
|
||||||
|
|
||||||
|
for i in 0..10 {
|
||||||
|
let file_name = format!("file{}.txt", i);
|
||||||
|
let write_res = write_file(&directory.file_path, file_name, "test");
|
||||||
|
assert!(write_res.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(files) = directory.get_files() {
|
||||||
|
for i in 0..10 {
|
||||||
|
let file_name = format!("file{}.txt", i);
|
||||||
|
assert!(files.contains_key(&file_name));
|
||||||
|
assert_eq!(files.get(&file_name).unwrap(), "test");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Files not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(file_names) = directory.get_file_names() {
|
||||||
|
for i in 0..10 {
|
||||||
|
let file_name = format!("file{}.txt", i);
|
||||||
|
assert!(file_names.contains(&file_name));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("File names not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..10 {
|
||||||
|
let file_name = format!("file{}.txt", i);
|
||||||
|
assert!(delete_file(&directory.file_path, file_name).is_ok());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("could not get interactor")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/storage/key_storage_impl.rs
Normal file
112
src/storage/key_storage_impl.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use enostr::{Keypair, Pubkey};
|
||||||
|
|
||||||
|
use super::file_key_storage::FileKeyStorage;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use super::security_framework_key_storage::SecurityFrameworkKeyStorage;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum KeyStorageType {
|
||||||
|
None,
|
||||||
|
FileSystem(FileKeyStorage),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
SecurityFramework(SecurityFrameworkKeyStorage),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum KeyStorageResponse<R> {
|
||||||
|
Waiting,
|
||||||
|
ReceivedResult(Result<R, KeyStorageError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: PartialEq> PartialEq for KeyStorageResponse<R> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(KeyStorageResponse::Waiting, KeyStorageResponse::Waiting) => true,
|
||||||
|
(
|
||||||
|
KeyStorageResponse::ReceivedResult(Ok(r1)),
|
||||||
|
KeyStorageResponse::ReceivedResult(Ok(r2)),
|
||||||
|
) => r1 == r2,
|
||||||
|
(
|
||||||
|
KeyStorageResponse::ReceivedResult(Err(_)),
|
||||||
|
KeyStorageResponse::ReceivedResult(Err(_)),
|
||||||
|
) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyStorageType {
|
||||||
|
pub fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
|
||||||
|
match self {
|
||||||
|
Self::None => KeyStorageResponse::ReceivedResult(Ok(Vec::new())),
|
||||||
|
Self::FileSystem(f) => f.get_keys(),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
Self::SecurityFramework(f) => f.get_keys(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
||||||
|
let _ = key;
|
||||||
|
match self {
|
||||||
|
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
||||||
|
Self::FileSystem(f) => f.add_key(key),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
Self::SecurityFramework(f) => f.add_key(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
||||||
|
let _ = key;
|
||||||
|
match self {
|
||||||
|
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
||||||
|
Self::FileSystem(f) => f.remove_key(key),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
Self::SecurityFramework(f) => f.remove_key(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_key(&self) -> KeyStorageResponse<Option<Pubkey>> {
|
||||||
|
match self {
|
||||||
|
Self::None => KeyStorageResponse::ReceivedResult(Ok(None)),
|
||||||
|
Self::FileSystem(f) => f.get_selected_key(),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
Self::SecurityFramework(_) => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_key(&self, key: Option<Pubkey>) -> KeyStorageResponse<()> {
|
||||||
|
match self {
|
||||||
|
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
||||||
|
Self::FileSystem(f) => f.select_key(key),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
Self::SecurityFramework(_) => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum KeyStorageError {
|
||||||
|
Retrieval(Error),
|
||||||
|
Addition(Error),
|
||||||
|
Selection(Error),
|
||||||
|
Removal(Error),
|
||||||
|
OSError(Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for KeyStorageError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Retrieval(e) => write!(f, "Failed to retrieve keys: {:?}", e),
|
||||||
|
Self::Addition(key) => write!(f, "Failed to add key: {:?}", key),
|
||||||
|
Self::Selection(pubkey) => write!(f, "Failed to select key: {:?}", pubkey),
|
||||||
|
Self::Removal(key) => write!(f, "Failed to remove key: {:?}", key),
|
||||||
|
Self::OSError(e) => write!(f, "OS had an error: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for KeyStorageError {}
|
||||||
14
src/storage/mod.rs
Normal file
14
src/storage/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
mod file_key_storage;
|
||||||
|
mod file_storage;
|
||||||
|
|
||||||
|
pub use file_key_storage::FileKeyStorage;
|
||||||
|
pub use file_storage::write_file;
|
||||||
|
pub use file_storage::DataPaths;
|
||||||
|
pub use file_storage::Directory;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod security_framework_key_storage;
|
||||||
|
|
||||||
|
pub mod key_storage_impl;
|
||||||
|
pub use key_storage_impl::{KeyStorageResponse, KeyStorageType};
|
||||||
@@ -1,40 +1,45 @@
|
|||||||
#![cfg(target_os = "macos")]
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use enostr::{Keypair, Pubkey, SecretKey};
|
use enostr::{Keypair, Pubkey, SecretKey};
|
||||||
|
use security_framework::{
|
||||||
use security_framework::item::{ItemClass, ItemSearchOptions, Limit, SearchResult};
|
item::{ItemClass, ItemSearchOptions, Limit, SearchResult},
|
||||||
use security_framework::passwords::{delete_generic_password, set_generic_password};
|
passwords::{delete_generic_password, set_generic_password},
|
||||||
|
};
|
||||||
use crate::key_storage::{KeyStorage, KeyStorageError, KeyStorageResponse};
|
|
||||||
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
pub struct MacOSKeyStorage<'a> {
|
use crate::Error;
|
||||||
pub service_name: &'a str,
|
|
||||||
|
use super::{key_storage_impl::KeyStorageError, KeyStorageResponse};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct SecurityFrameworkKeyStorage {
|
||||||
|
pub service_name: Cow<'static, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MacOSKeyStorage<'a> {
|
impl SecurityFrameworkKeyStorage {
|
||||||
pub fn new(service_name: &'a str) -> Self {
|
pub fn new(service_name: String) -> Self {
|
||||||
MacOSKeyStorage { service_name }
|
SecurityFrameworkKeyStorage {
|
||||||
|
service_name: Cow::Owned(service_name),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_key(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
||||||
match set_generic_password(
|
match set_generic_password(
|
||||||
self.service_name,
|
&self.service_name,
|
||||||
key.pubkey.hex().as_str(),
|
key.pubkey.hex().as_str(),
|
||||||
key.secret_key
|
key.secret_key
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or_else(|| &[] as &[u8], |sc| sc.as_secret_bytes()),
|
.map_or_else(|| &[] as &[u8], |sc| sc.as_secret_bytes()),
|
||||||
) {
|
) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(_) => Err(KeyStorageError::Addition(key.pubkey.hex())),
|
Err(e) => Err(KeyStorageError::Addition(Error::Generic(e.to_string()))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_pubkey_strings(&self) -> Vec<String> {
|
fn get_pubkey_strings(&self) -> Vec<String> {
|
||||||
let search_results = ItemSearchOptions::new()
|
let search_results = ItemSearchOptions::new()
|
||||||
.class(ItemClass::generic_password())
|
.class(ItemClass::generic_password())
|
||||||
.service(self.service_name)
|
.service(&self.service_name)
|
||||||
.load_attributes(true)
|
.load_attributes(true)
|
||||||
.limit(Limit::All)
|
.limit(Limit::All)
|
||||||
.search();
|
.search();
|
||||||
@@ -64,7 +69,7 @@ impl<'a> MacOSKeyStorage<'a> {
|
|||||||
fn get_privkey_bytes_for(&self, account: &str) -> Option<Vec<u8>> {
|
fn get_privkey_bytes_for(&self, account: &str) -> Option<Vec<u8>> {
|
||||||
let search_result = ItemSearchOptions::new()
|
let search_result = ItemSearchOptions::new()
|
||||||
.class(ItemClass::generic_password())
|
.class(ItemClass::generic_password())
|
||||||
.service(self.service_name)
|
.service(&self.service_name)
|
||||||
.load_data(true)
|
.load_data(true)
|
||||||
.account(account)
|
.account(account)
|
||||||
.search();
|
.search();
|
||||||
@@ -97,26 +102,26 @@ impl<'a> MacOSKeyStorage<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn delete_key(&self, pubkey: &Pubkey) -> Result<(), KeyStorageError> {
|
fn delete_key(&self, pubkey: &Pubkey) -> Result<(), KeyStorageError> {
|
||||||
match delete_generic_password(self.service_name, pubkey.hex().as_str()) {
|
match delete_generic_password(&self.service_name, pubkey.hex().as_str()) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("delete key error {}", e);
|
error!("delete key error {}", e);
|
||||||
Err(KeyStorageError::Removal(pubkey.hex()))
|
Err(KeyStorageError::Removal(Error::Generic(e.to_string())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> KeyStorage for MacOSKeyStorage<'a> {
|
impl SecurityFrameworkKeyStorage {
|
||||||
fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
pub fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
||||||
KeyStorageResponse::ReceivedResult(self.add_key(key))
|
KeyStorageResponse::ReceivedResult(self.add_key_internal(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
|
pub fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
|
||||||
KeyStorageResponse::ReceivedResult(Ok(self.get_all_keypairs()))
|
KeyStorageResponse::ReceivedResult(Ok(self.get_all_keypairs()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
pub fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
||||||
KeyStorageResponse::ReceivedResult(self.delete_key(&key.pubkey))
|
KeyStorageResponse::ReceivedResult(self.delete_key(&key.pubkey))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,8 +132,8 @@ mod tests {
|
|||||||
use enostr::FullKeypair;
|
use enostr::FullKeypair;
|
||||||
|
|
||||||
static TEST_SERVICE_NAME: &str = "NOTEDECKTEST";
|
static TEST_SERVICE_NAME: &str = "NOTEDECKTEST";
|
||||||
static STORAGE: MacOSKeyStorage = MacOSKeyStorage {
|
static STORAGE: SecurityFrameworkKeyStorage = SecurityFrameworkKeyStorage {
|
||||||
service_name: TEST_SERVICE_NAME,
|
service_name: Cow::Borrowed(TEST_SERVICE_NAME),
|
||||||
};
|
};
|
||||||
|
|
||||||
// individual tests are ignored so test runner doesn't run them all concurrently
|
// individual tests are ignored so test runner doesn't run them all concurrently
|
||||||
@@ -140,14 +145,14 @@ mod tests {
|
|||||||
let num_keys_before_test = STORAGE.get_pubkeys().len();
|
let num_keys_before_test = STORAGE.get_pubkeys().len();
|
||||||
|
|
||||||
let keypair = FullKeypair::generate().to_keypair();
|
let keypair = FullKeypair::generate().to_keypair();
|
||||||
let add_result = STORAGE.add_key(&keypair);
|
let add_result = STORAGE.add_key_internal(&keypair);
|
||||||
assert_eq!(add_result, Ok(()));
|
assert!(add_result.is_ok());
|
||||||
|
|
||||||
let get_pubkeys_result = STORAGE.get_pubkeys();
|
let get_pubkeys_result = STORAGE.get_pubkeys();
|
||||||
assert_eq!(get_pubkeys_result.len() - num_keys_before_test, 1);
|
assert_eq!(get_pubkeys_result.len() - num_keys_before_test, 1);
|
||||||
|
|
||||||
let remove_result = STORAGE.delete_key(&keypair.pubkey);
|
let remove_result = STORAGE.delete_key(&keypair.pubkey);
|
||||||
assert_eq!(remove_result, Ok(()));
|
assert!(remove_result.is_ok());
|
||||||
|
|
||||||
let keys = STORAGE.get_pubkeys();
|
let keys = STORAGE.get_pubkeys();
|
||||||
assert_eq!(keys.len() - num_keys_before_test, 0);
|
assert_eq!(keys.len() - num_keys_before_test, 0);
|
||||||
@@ -163,8 +168,8 @@ mod tests {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
expected_keypairs.iter().for_each(|keypair| {
|
expected_keypairs.iter().for_each(|keypair| {
|
||||||
let add_result = STORAGE.add_key(keypair);
|
let add_result = STORAGE.add_key_internal(keypair);
|
||||||
assert_eq!(add_result, Ok(()));
|
assert!(add_result.is_ok());
|
||||||
});
|
});
|
||||||
|
|
||||||
let asserted_keypairs = STORAGE.get_all_keypairs();
|
let asserted_keypairs = STORAGE.get_all_keypairs();
|
||||||
@@ -172,7 +177,7 @@ mod tests {
|
|||||||
|
|
||||||
expected_keypairs.iter().for_each(|keypair| {
|
expected_keypairs.iter().for_each(|keypair| {
|
||||||
let remove_result = STORAGE.delete_key(&keypair.pubkey);
|
let remove_result = STORAGE.delete_key(&keypair.pubkey);
|
||||||
assert_eq!(remove_result, Ok(()));
|
assert!(remove_result.is_ok());
|
||||||
});
|
});
|
||||||
|
|
||||||
let num_keys_after_test = STORAGE.get_all_keypairs().len();
|
let num_keys_after_test = STORAGE.get_all_keypairs().len();
|
||||||
158
src/support.rs
Normal file
158
src/support.rs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::{storage::Directory, DataPaths};
|
||||||
|
|
||||||
|
pub struct Support {
|
||||||
|
directory: Option<Directory>,
|
||||||
|
mailto_url: String,
|
||||||
|
most_recent_log: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_log_dir() -> Option<Directory> {
|
||||||
|
match DataPaths::Log.get_path() {
|
||||||
|
Ok(path) => Some(Directory::new(path)),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Support could not open directory: {}", e.to_string());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Support {
|
||||||
|
fn default() -> Self {
|
||||||
|
let directory = new_log_dir();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
mailto_url: MailtoBuilder::new(SUPPORT_EMAIL.to_string())
|
||||||
|
.with_subject("Help Needed".to_owned())
|
||||||
|
.with_content(EMAIL_TEMPLATE.to_owned())
|
||||||
|
.build(),
|
||||||
|
directory,
|
||||||
|
most_recent_log: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static MAX_LOG_LINES: usize = 500;
|
||||||
|
static SUPPORT_EMAIL: &str = "support@damus.io";
|
||||||
|
static EMAIL_TEMPLATE: &str = "Describe the bug you have encountered:\n<-- your statement here -->\n\n===== Paste your log below =====\n\n";
|
||||||
|
|
||||||
|
impl Support {
|
||||||
|
pub fn refresh(&mut self) {
|
||||||
|
if let Some(directory) = &self.directory {
|
||||||
|
self.most_recent_log = get_log_str(directory);
|
||||||
|
} else {
|
||||||
|
self.directory = new_log_dir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mailto_url(&self) -> &str {
|
||||||
|
&self.mailto_url
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_log_dir(&self) -> Option<&str> {
|
||||||
|
self.directory.as_ref()?.file_path.to_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_most_recent_log(&self) -> Option<&String> {
|
||||||
|
self.most_recent_log.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_log_str(interactor: &Directory) -> Option<String> {
|
||||||
|
match interactor.get_most_recent() {
|
||||||
|
Ok(Some(most_recent_name)) => {
|
||||||
|
match interactor.get_file_last_n_lines(most_recent_name.clone(), MAX_LOG_LINES) {
|
||||||
|
Ok(file_output) => {
|
||||||
|
return Some(
|
||||||
|
get_prefix(
|
||||||
|
&most_recent_name,
|
||||||
|
file_output.output_num_lines,
|
||||||
|
file_output.total_lines_in_file,
|
||||||
|
) + &file_output.output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Error retrieving the last lines from file {}: {:?}",
|
||||||
|
most_recent_name, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
error!("No files were found.");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error fetching the most recent file: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_prefix(file_name: &str, lines_displayed: usize, num_total_lines: usize) -> String {
|
||||||
|
format!(
|
||||||
|
"===\nDisplaying the last {} of {} lines in file {}\n===\n\n",
|
||||||
|
lines_displayed, num_total_lines, file_name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MailtoBuilder {
|
||||||
|
content: Option<String>,
|
||||||
|
address: String,
|
||||||
|
subject: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MailtoBuilder {
|
||||||
|
fn new(address: String) -> Self {
|
||||||
|
Self {
|
||||||
|
content: None,
|
||||||
|
address,
|
||||||
|
subject: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// will be truncated so the whole URL is at most 2000 characters
|
||||||
|
pub fn with_content(mut self, content: String) -> Self {
|
||||||
|
self.content = Some(content);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_subject(mut self, subject: String) -> Self {
|
||||||
|
self.subject = Some(subject);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> String {
|
||||||
|
let mut url = String::new();
|
||||||
|
|
||||||
|
url.push_str("mailto:");
|
||||||
|
url.push_str(&self.address);
|
||||||
|
|
||||||
|
let has_subject = self.subject.is_some();
|
||||||
|
|
||||||
|
if has_subject || self.content.is_some() {
|
||||||
|
url.push('?');
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(subject) = self.subject {
|
||||||
|
url.push_str("subject=");
|
||||||
|
url.push_str(&urlencoding::encode(&subject));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(content) = self.content {
|
||||||
|
if has_subject {
|
||||||
|
url.push('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
url.push_str("body=");
|
||||||
|
|
||||||
|
let body = urlencoding::encode(&content);
|
||||||
|
|
||||||
|
url.push_str(&body);
|
||||||
|
}
|
||||||
|
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/ui/button_hyperlink.rs
Normal file
49
src/ui/button_hyperlink.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use egui::{Button, Response, Ui, Widget};
|
||||||
|
|
||||||
|
pub struct ButtonHyperlink<'a> {
|
||||||
|
url: String,
|
||||||
|
button: Button<'a>,
|
||||||
|
new_tab: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ButtonHyperlink<'a> {
|
||||||
|
pub fn new(button: Button<'a>, url: impl ToString) -> Self {
|
||||||
|
let url = url.to_string();
|
||||||
|
Self {
|
||||||
|
url: url.clone(),
|
||||||
|
button,
|
||||||
|
new_tab: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_in_new_tab(mut self, new_tab: bool) -> Self {
|
||||||
|
self.new_tab = new_tab;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for ButtonHyperlink<'a> {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
let response = ui.add(self.button);
|
||||||
|
|
||||||
|
if response.clicked() {
|
||||||
|
let modifiers = ui.ctx().input(|i| i.modifiers);
|
||||||
|
ui.ctx().open_url(egui::OpenUrl {
|
||||||
|
url: self.url.clone(),
|
||||||
|
new_tab: self.new_tab || modifiers.any(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if response.middle_clicked() {
|
||||||
|
ui.ctx().open_url(egui::OpenUrl {
|
||||||
|
url: self.url.clone(),
|
||||||
|
new_tab: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.style().url_in_tooltip {
|
||||||
|
response.on_hover_text(self.url)
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,14 @@ pub mod account_login_view;
|
|||||||
pub mod account_management;
|
pub mod account_management;
|
||||||
pub mod add_column;
|
pub mod add_column;
|
||||||
pub mod anim;
|
pub mod anim;
|
||||||
|
pub mod button_hyperlink;
|
||||||
pub mod mention;
|
pub mod mention;
|
||||||
pub mod note;
|
pub mod note;
|
||||||
pub mod preview;
|
pub mod preview;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod relay;
|
pub mod relay;
|
||||||
pub mod side_panel;
|
pub mod side_panel;
|
||||||
|
pub mod support;
|
||||||
pub mod thread;
|
pub mod thread;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
pub mod username;
|
pub mod username;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::{
|
|||||||
column::{Column, Columns},
|
column::{Column, Columns},
|
||||||
imgcache::ImageCache,
|
imgcache::ImageCache,
|
||||||
route::Route,
|
route::Route,
|
||||||
|
support::Support,
|
||||||
user_account::UserAccount,
|
user_account::UserAccount,
|
||||||
Damus,
|
Damus,
|
||||||
};
|
};
|
||||||
@@ -41,6 +42,7 @@ pub enum SidePanelAction {
|
|||||||
ComposeNote,
|
ComposeNote,
|
||||||
Search,
|
Search,
|
||||||
ExpandSidePanel,
|
ExpandSidePanel,
|
||||||
|
Support,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SidePanelResponse {
|
pub struct SidePanelResponse {
|
||||||
@@ -114,6 +116,8 @@ impl<'a> DesktopSidePanel<'a> {
|
|||||||
let pfp_resp = self.pfp_button(ui);
|
let pfp_resp = self.pfp_button(ui);
|
||||||
let settings_resp = ui.add(settings_button(dark_mode));
|
let settings_resp = ui.add(settings_button(dark_mode));
|
||||||
|
|
||||||
|
let support_resp = ui.add(support_button());
|
||||||
|
|
||||||
let optional_inner = if pfp_resp.clicked() {
|
let optional_inner = if pfp_resp.clicked() {
|
||||||
Some(egui::InnerResponse::new(
|
Some(egui::InnerResponse::new(
|
||||||
SidePanelAction::Account,
|
SidePanelAction::Account,
|
||||||
@@ -124,6 +128,11 @@ impl<'a> DesktopSidePanel<'a> {
|
|||||||
SidePanelAction::Settings,
|
SidePanelAction::Settings,
|
||||||
settings_resp,
|
settings_resp,
|
||||||
))
|
))
|
||||||
|
} else if support_resp.clicked() {
|
||||||
|
Some(egui::InnerResponse::new(
|
||||||
|
SidePanelAction::Support,
|
||||||
|
support_resp,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -162,7 +171,7 @@ impl<'a> DesktopSidePanel<'a> {
|
|||||||
helper.take_animation_response()
|
helper.take_animation_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn perform_action(columns: &mut Columns, action: SidePanelAction) {
|
pub fn perform_action(columns: &mut Columns, support: &mut Support, action: SidePanelAction) {
|
||||||
let router = columns.get_first_router();
|
let router = columns.get_first_router();
|
||||||
match action {
|
match action {
|
||||||
SidePanelAction::Panel => {} // TODO
|
SidePanelAction::Panel => {} // TODO
|
||||||
@@ -208,6 +217,14 @@ impl<'a> DesktopSidePanel<'a> {
|
|||||||
// TODO
|
// TODO
|
||||||
info!("Clicked expand side panel button");
|
info!("Clicked expand side panel button");
|
||||||
}
|
}
|
||||||
|
SidePanelAction::Support => {
|
||||||
|
if router.routes().iter().any(|&r| r == Route::Support) {
|
||||||
|
router.go_back();
|
||||||
|
} else {
|
||||||
|
support.refresh();
|
||||||
|
router.route_to(Route::Support);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,6 +369,28 @@ fn expand_side_panel_button() -> impl Widget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn support_button() -> impl Widget {
|
||||||
|
|ui: &mut egui::Ui| -> egui::Response {
|
||||||
|
let img_size = 16.0;
|
||||||
|
|
||||||
|
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
||||||
|
let img_data = egui::include_image!("../../assets/icons/help_icon_dark_4x.png");
|
||||||
|
let img = egui::Image::new(img_data).max_width(img_size);
|
||||||
|
|
||||||
|
let helper = AnimationHelper::new(ui, "help-button", vec2(max_size, max_size));
|
||||||
|
|
||||||
|
let cur_img_size = helper.scale_1d_pos(img_size);
|
||||||
|
img.paint_at(
|
||||||
|
ui,
|
||||||
|
helper
|
||||||
|
.get_animation_rect()
|
||||||
|
.shrink((max_size - cur_img_size) / 2.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
helper.take_animation_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod preview {
|
mod preview {
|
||||||
|
|
||||||
use egui_extras::{Size, StripBuilder};
|
use egui_extras::{Size, StripBuilder};
|
||||||
@@ -390,7 +429,11 @@ mod preview {
|
|||||||
);
|
);
|
||||||
let response = panel.show(ui);
|
let response = panel.show(ui);
|
||||||
|
|
||||||
DesktopSidePanel::perform_action(&mut self.app.columns, response.action);
|
DesktopSidePanel::perform_action(
|
||||||
|
&mut self.app.columns,
|
||||||
|
&mut self.app.support,
|
||||||
|
response.action,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/ui/support.rs
Normal file
76
src/ui/support.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use egui::{vec2, Button, Label, Layout, RichText};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app_style::{get_font_size, NotedeckTextStyle},
|
||||||
|
colors::PINK,
|
||||||
|
fonts::NamedFontFamily,
|
||||||
|
support::Support,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{button_hyperlink::ButtonHyperlink, padding};
|
||||||
|
|
||||||
|
pub struct SupportView<'a> {
|
||||||
|
support: &'a mut Support,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SupportView<'a> {
|
||||||
|
pub fn new(support: &'a mut Support) -> Self {
|
||||||
|
Self { support }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self, ui: &mut egui::Ui) {
|
||||||
|
padding(8.0, ui, |ui| {
|
||||||
|
ui.spacing_mut().item_spacing = egui::vec2(0.0, 8.0);
|
||||||
|
let font = egui::FontId::new(
|
||||||
|
get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
|
||||||
|
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||||
|
);
|
||||||
|
ui.add(Label::new(RichText::new("Running into a bug?").font(font)));
|
||||||
|
ui.label(RichText::new("Step 1").text_style(NotedeckTextStyle::Heading3.text_style()));
|
||||||
|
padding(8.0, ui, |ui| {
|
||||||
|
ui.label("Open your default email client to get help from the Damus team");
|
||||||
|
let size = vec2(120.0, 40.0);
|
||||||
|
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
|
||||||
|
ui.add(ButtonHyperlink::new(
|
||||||
|
Button::new(
|
||||||
|
RichText::new("Open Email")
|
||||||
|
.size(get_font_size(ui.ctx(), &NotedeckTextStyle::Body)),
|
||||||
|
)
|
||||||
|
.fill(PINK)
|
||||||
|
.min_size(size),
|
||||||
|
self.support.get_mailto_url(),
|
||||||
|
));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
if let Some(logs) = self.support.get_most_recent_log() {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Step 2").text_style(NotedeckTextStyle::Heading3.text_style()),
|
||||||
|
);
|
||||||
|
let size = vec2(80.0, 40.0);
|
||||||
|
let copy_button = Button::new(
|
||||||
|
RichText::new("Copy").size(get_font_size(ui.ctx(), &NotedeckTextStyle::Body)),
|
||||||
|
)
|
||||||
|
.fill(PINK)
|
||||||
|
.min_size(size);
|
||||||
|
padding(8.0, ui, |ui| {
|
||||||
|
ui.add(Label::new("Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email.").wrap());
|
||||||
|
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
|
||||||
|
if ui.add(copy_button).clicked() {
|
||||||
|
ui.output_mut(|w| {
|
||||||
|
w.copied_text = logs.to_string();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new("ERROR: Could not find logs on system")
|
||||||
|
.color(egui::Color32::RED),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user