mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
feat(fuse,core,gui,cli): one-request move+rename drain, show who shared an item
- A queued rename that also changes folder now lands as one `move-multiple` request carrying the target name instead of rename-then-move, so there is no half-applied state and no second call for stale name requirements to hit (B46 follow-up). A destination name collision moves under a conflict name in one step; same-folder renames still use the rename endpoint. - Shared with me shows the inviter and invite date (GUI subtitle, `pdfs shared-with-me`), with a warning when the invitation signature does not verify against the claimed inviter's keys. `DirEntry` gains `shared_by`, `shared_at` and `shared_by_unverified`, all serde-defaulted. - Bump `proton-sdk` / `proton-drive-rs` to 0.6.3 (anonymous-node moves, `TrashTime` trashed detection).
This commit is contained in:
1 parent
c28803b037
commit
eb56f4a1e0
17 files changed
+315
-53
No files matched your search
@@ -2345,6 +2345,9 @@ pub(crate) fn repaint_search(ui: &Rc<Ui>, hits: &[SearchHit]) {
|
||||
uid: h.uid.clone(),
|
||||
path: h.path.clone(),
|
||||
role: String::new(),
|
||||
shared_by: String::new(),
|
||||
shared_at: 0,
|
||||
shared_by_unverified: false,
|
||||
})
|
||||
.collect();
|
||||
entries.sort_by(|a, b| {
|
||||
|
||||
@@ -309,6 +309,17 @@ fn shared_entry_row(ui: &Rc<Ui>, entry: &DirEntry) -> adw::ActionRow {
|
||||
if let Some(badge) = role_badge(&entry.role) {
|
||||
row.add_suffix(&badge);
|
||||
}
|
||||
// The "shared by" in the subtitle is what the invitation claims. When its
|
||||
// signature does not check out against that person's keys, the claim must
|
||||
// not read as fact — someone may be impersonating a contact to get a file
|
||||
// opened.
|
||||
if entry.shared_by_unverified {
|
||||
let warning = gtk4::Image::from_icon_name("dialog-warning-symbolic");
|
||||
warning.add_css_class("warning");
|
||||
warning.set_valign(gtk4::Align::Center);
|
||||
warning.set_tooltip_text(Some(UNVERIFIED_INVITER_TOOLTIP));
|
||||
row.add_suffix(&warning);
|
||||
}
|
||||
row.add_prefix(&file_thumbnail(ui, entry, 40, 24, true));
|
||||
let ui_act = ui.clone();
|
||||
let uid = entry.uid.clone();
|
||||
@@ -329,12 +340,29 @@ fn shared_entry_row(ui: &Rc<Ui>, entry: &DirEntry) -> adw::ActionRow {
|
||||
row
|
||||
}
|
||||
|
||||
/// A shared row's subtitle: its size (files only), and where it can be reached
|
||||
/// in the local mount once the synthetic `Shared with me/` directory has interned
|
||||
/// it. An unlisted share has no local path yet, and says nothing rather than
|
||||
/// pointing at a directory that is not there.
|
||||
/// Tooltip on the warning shown next to a share whose invitation did not verify.
|
||||
const UNVERIFIED_INVITER_TOOLTIP: &str = "The invitation's signature doesn't match the sender's keys. \
|
||||
It may not really be from them.";
|
||||
|
||||
/// A shared row's subtitle: who shared it and when (share roots only — that is
|
||||
/// where the invitation lives), its size (files only), and where it can be
|
||||
/// reached in the local mount once the synthetic `Shared with me/` directory has
|
||||
/// interned it. An unlisted share has no local path yet, and says nothing rather
|
||||
/// than pointing at a directory that is not there.
|
||||
pub(crate) fn shared_row_subtitle(entry: &DirEntry) -> String {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
if !entry.shared_by.is_empty() {
|
||||
let date = if entry.shared_at > 0 {
|
||||
format_modified(entry.shared_at)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
parts.push(if date.is_empty() {
|
||||
format!("Shared by {}", entry.shared_by)
|
||||
} else {
|
||||
format!("Shared by {} on {date}", entry.shared_by)
|
||||
});
|
||||
}
|
||||
if !entry.is_dir {
|
||||
parts.push(human_bytes(entry.size));
|
||||
}
|
||||
@@ -723,9 +751,25 @@ mod tests {
|
||||
uid: "vol~link".into(),
|
||||
path: path.into(),
|
||||
role: role.into(),
|
||||
shared_by: String::new(),
|
||||
shared_at: 0,
|
||||
shared_by_unverified: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_share_root_says_who_shared_it() {
|
||||
let mut root = entry(true, "", "viewer");
|
||||
root.shared_by = "alice@proton.me".into();
|
||||
assert_eq!(shared_row_subtitle(&root), "Shared by alice@proton.me");
|
||||
root.shared_at = 1_700_000_000;
|
||||
let subtitle = shared_row_subtitle(&root);
|
||||
assert!(
|
||||
subtitle.starts_with("Shared by alice@proton.me on "),
|
||||
"{subtitle}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_share_with_no_local_path_says_nothing_about_one() {
|
||||
// Before the synthetic directory has been listed, the daemon cannot
|
||||
|
||||
@@ -293,5 +293,8 @@ pub(crate) fn shared_item_as_entry(item: &SharedItem) -> DirEntry {
|
||||
path: item.path.clone(),
|
||||
// Shared *by* me: I own it, so there is no role of mine to report.
|
||||
role: String::new(),
|
||||
shared_by: String::new(),
|
||||
shared_at: 0,
|
||||
shared_by_unverified: false,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user